16 Commits

Author SHA1 Message Date
357eecabf9 feat: Updates tests for html generation, adds some more documentation strings.
All checks were successful
CI / Run Tests (pull_request) Successful in 4m25s
Build docker images / docker (pull_request) Successful in 7m31s
2025-11-17 10:19:11 -05:00
045f48348d feat: Fixes generation of html document, it now builds a latex document in the build directory that it then converts to html. Need to update tests. 2025-11-17 09:38:23 -05:00
8ee4e436aa feat: Adds /root/project directory and sets it as the working directory in docker container.
All checks were successful
CI / Run Tests (push) Successful in 1m25s
Build docker images / docker (push) Successful in 7m22s
Release / release (push) Successful in 9s
2025-11-13 15:22:48 -05:00
064976ed6e feat: Fixes entrypoint script that was not appropriately handling arguments to hpa script.
All checks were successful
CI / Run Tests (push) Successful in 2m31s
Build docker images / docker (push) Successful in 7m26s
2025-11-13 14:59:35 -05:00
45a1520a2b feat: Adds xelatex package to docker image.
All checks were successful
CI / Run Tests (push) Successful in 1m25s
Build docker images / docker (push) Successful in 7m21s
2025-11-07 17:02:50 -05:00
f7f3ac5dc7 feat: Updates Dockerfile, WIP
All checks were successful
CI / Run Tests (push) Successful in 1m27s
Build docker images / docker (push) Successful in 6m45s
2025-11-07 15:16:05 -05:00
15454e3686 feat: Adds entrypoint.sh script to docker to allow attaching to a shell inside the container.
All checks were successful
CI / Run Tests (push) Successful in 2m33s
Build docker images / docker (push) Successful in 6m49s
2025-11-07 13:18:57 -05:00
63012131d4 feat: Removes image creation on dev branch, only create for main branch.
All checks were successful
CI / Run Tests (pull_request) Successful in 1m25s
Build docker images / docker (pull_request) Successful in 6m44s
CI / Run Tests (push) Successful in 1m26s
Build docker images / docker (push) Successful in 6m48s
2025-11-04 14:09:30 -05:00
a013e6fe81 feat: Adds docker ci workflow. 2025-11-04 14:09:30 -05:00
9d8f3368ec feat: Adds todos, updates Dockerfile to use hpa as the entrypoint. 2025-11-04 14:09:30 -05:00
88a1f181cb feat: Updates Dockerfile, WIP 2025-11-04 14:09:30 -05:00
a6227a80db feat: Updates release notes. 2025-11-04 14:09:29 -05:00
d1a47e2ac6 feat: Updates to check if playbook is installed prior to running any of the commands, updates tests. 2025-11-04 14:09:29 -05:00
86dc084e7d feat: Documenting release workflow. 2025-11-04 14:09:29 -05:00
d3a9aa2f00 feat: More readme, also removes ci from dev branch and only runs for pulls and pushes to main. 2025-11-04 14:08:29 -05:00
1820988894 feat: Fix merge conflicts. 2025-11-04 14:06:33 -05:00
15 changed files with 418 additions and 113 deletions

View File

@@ -0,0 +1,55 @@
name: Build docker images
on:
push:
branches:
- main
pull_request: {}
workflow_dispatch: {}
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
lfs: true
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
- name: Setup docker buildx
uses: docker/setup-buildx-action@v3
- name: Login to Container Registery
uses: docker/login-action@v3
with:
registry: git.housh.dev
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: git.housh.dev/michael/swift-hpa
tags: |
type=schedule
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
type=raw,value=latest
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
file: ./docker/Dockerfile
platforms: linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -21,7 +21,7 @@ brew install michael/formula/hpa
Installation on platforms other than `macOS` are currently being worked on, along with support for
running in a `docker` container.
### Ensuring dependencies are installed.
### Ensuring dependencies are installed
This application requires some dependencies to be installed on your system, you can install the
dependencies with the following command.
@@ -44,7 +44,7 @@ repositories, and encrypt / decrypt variable files. The playbook get's installed
> NOTE: All commands accept a `--help` option which will display the arguments and options a command
> can use, along with example usage of the commands.
### Configure the application.
### Configure the application
When you first download the application you can setup the configuration file for your use case.
@@ -76,7 +76,7 @@ allows your template to expand over time.
Once your template is setup, make sure that your configuration file is setup to point to your
customized template.
## Creating a project.
## Creating a project
The first step after having your template defined is to create a project that uses it. The below
command will create a template in the `~/consults/my-first-consult` directory.
@@ -99,7 +99,7 @@ Or if your configuration has `directory` set in the `template` section.
hpa create --use-local-template ~/consults/my-first-consult
```
## Generating output files.
## Generating output files
Once you have created a project and edited the contents to your liking. You can then generate the
final output file (typically a pdf) that can be sent to your customer.
@@ -122,7 +122,7 @@ Currently the supported output file types are:
1. LaTeX
1. HTML
## Build command.
## Build command
The command line tool goes through an intermediate step when generating output, which is called
`build`. The build step generates the final output files using defined variables that are located in
@@ -147,7 +147,7 @@ like to generate output for.
hpa build --project-directory ~/consults/my-first-consult
```
## Some General Usage Notes:
## Some General Usage Notes
There is often a lot of output to the console when running commands, which can be problematic if you
want to pipe the output into other command line tools, so all options accept a `-q | --quiet` flag

24
Release.md Normal file
View File

@@ -0,0 +1,24 @@
# 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/hpa/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. 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

@@ -4,10 +4,10 @@ import DependenciesMacros
import Foundation
import ShellClient
public extension DependencyValues {
extension DependencyValues {
/// Runs shell commands.
var commandClient: CommandClient {
public var commandClient: CommandClient {
get { self[CommandClient.self] }
set { self[CommandClient.self] = newValue }
}
@@ -67,12 +67,13 @@ public struct CommandClient: Sendable {
in workingDirectory: String? = nil,
_ arguments: [String]
) async throws {
try await runCommand(.init(
arguments: arguments,
quiet: quiet,
shell: shell,
workingDirectory: workingDirectory
))
try await runCommand(
.init(
arguments: arguments,
quiet: quiet,
shell: shell,
workingDirectory: workingDirectory
))
}
/// Runs a shell command.
@@ -161,19 +162,21 @@ extension CommandClient: DependencyKey {
.init { options in
@Dependency(\.asyncShellClient) var shellClient
if !options.quiet {
try await shellClient.foreground(.init(
shell: .init(options.shell),
environment: environment,
in: options.workingDirectory,
options.arguments
))
try await shellClient.foreground(
.init(
shell: .init(options.shell),
environment: environment,
in: options.workingDirectory,
options.arguments
))
} else {
try await shellClient.background(.init(
shell: .init(options.shell),
environment: environment,
in: options.workingDirectory,
options.arguments
))
try await shellClient.background(
.init(
shell: .init(options.shell),
environment: environment,
in: options.workingDirectory,
options.arguments
))
}
}
}
@@ -184,12 +187,12 @@ extension CommandClient: DependencyKey {
}
@_spi(Internal)
public extension CommandClient {
extension CommandClient {
/// Create a command client that can capture the arguments / options.
///
/// This is used for testing.
static func capturing(_ client: CapturingClient) -> Self {
public static func capturing(_ client: CapturingClient) -> Self {
.init { options in
await client.set(options)
}
@@ -198,7 +201,7 @@ public extension CommandClient {
/// Captures the arguments / options passed into the command client's run commands.
///
@dynamicMemberLookup
actor CapturingClient: Sendable {
public actor CapturingClient: Sendable {
public private(set) var options: RunCommandOptions?
public init() {}

View File

@@ -120,7 +120,11 @@ struct LiveFileClient: Sendable {
func isDirectory(_ url: URL) -> Bool {
var isDirectory: ObjCBool = false
manager.fileExists(atPath: url.cleanFilePath, isDirectory: &isDirectory)
#if os(Linux)
_ = manager.fileExists(atPath: url.cleanFilePath, isDirectory: &isDirectory)
#else
manager.fileExists(atPath: url.cleanFilePath, isDirectory: &isDirectory)
#endif
return isDirectory.boolValue
}

View File

@@ -6,26 +6,72 @@ import PlaybookClient
extension PandocClient.RunOptions {
/// Runs a pandoc conversion on the project generating the given file type.
///
/// - Parameters:
/// - fileType: The file type to convert to.
/// - environment: The environment variables.
///
/// - Returns: File path to the converted output file.
func run(
_ fileType: PandocClient.FileType,
_ environment: [String: String]
) async throws -> String {
@Dependency(\.logger) var logger
let ensuredOptions = try await self.ensuredOptions(fileType)
let projectDirectory = self.projectDirectory ?? environment["PWD"]
guard let projectDirectory else {
throw ProjectDirectoryNotSpecified()
}
try await buildProject(projectDirectory, ensuredOptions)
let outputDirectory = self.outputDirectory ?? projectDirectory
let outputPath = "\(outputDirectory)/\(ensuredOptions.ensuredFilename)"
let arguments = ensuredOptions.makeArguments(
outputPath: outputPath,
projectDirectory: projectDirectory
)
logger.debug("Pandoc arguments: \(arguments)")
return try await runCommand(arguments, outputPath)
}
/// Runs a shell command with the given arguments, returning the passed in output path
/// so the command can be chained, if needed.
///
@discardableResult
func runCommand(
_ arguments: [String],
_ outputPath: String
) async throws -> String {
@Dependency(\.commandClient) var commandClient
@Dependency(\.logger) var logger
logger.debug("Running shell command with arguments: \(arguments)")
return try await commandClient.run(logging: loggingOptions, quiet: quiet, shell: shell) {
(arguments, outputPath)
}
}
/// Build the project if necessary, before running the shell command that builds the final
/// output file(s).
///
func buildProject(
_ projectDirectory: String,
_ ensuredOptions: EnsuredPandocOptions
) async throws {
@Dependency(\.logger) var logger
@Dependency(\.playbookClient) var playbookClient
return try await commandClient.run(logging: loggingOptions, quiet: quiet, shell: shell) {
let ensuredOptions = try await self.ensuredOptions(fileType)
let projectDirectory = self.projectDirectory ?? environment["PWD"]
guard let projectDirectory else {
throw ProjectDirectoryNotSpecified()
}
if shouldBuildProject {
logger.debug("Building project...")
try await playbookClient.run.buildProject(.init(
if shouldBuildProject {
logger.debug("Building project...")
try await playbookClient.run.buildProject(
.init(
projectDirectory: projectDirectory,
shared: .init(
extraOptions: nil,
@@ -34,23 +80,28 @@ extension PandocClient.RunOptions {
quiet: quiet,
shell: shell
)
))
}
)
)
}
let outputDirectory = self.outputDirectory ?? projectDirectory
let outputPath = "\(outputDirectory)/\(ensuredOptions.ensuredExtensionFileName)"
let arguments = ensuredOptions.makeArguments(
// Build latex file pre-html, so that we can properly convert the latex document
// into an html document.
if ensuredOptions.outputFileType == .html {
logger.debug("Building latex, pre-html conversion...")
let outputPath = "\(ensuredOptions.buildDirectory)/\(EnsuredPandocOptions.latexFilename)"
let arguments = ensuredOptions.preHtmlLatexOptions.makeArguments(
outputPath: outputPath,
projectDirectory: projectDirectory
)
logger.debug("Pandoc arguments: \(arguments)")
return (arguments, outputPath)
try await runCommand(arguments, outputPath)
}
}
/// Generates the ensured/parsed options for a pandoc conversion.
///
/// - Parameter fileType: The file type we're converting to.
///
/// - Returns: The ensured options.
func ensuredOptions(
_ fileType: PandocClient.FileType
) async throws -> EnsuredPandocOptions {
@@ -69,6 +120,7 @@ extension PandocClient.RunOptions {
}
extension PandocClient.FileType {
/// Represents the appropriate file extension for a file type.
var fileExtension: String {
switch self {
case .html: return "html"
@@ -78,8 +130,14 @@ extension PandocClient.FileType {
}
}
/// Represents pandoc options that get parsed based on the given run options, configuration, etc.
///
/// This set's potentially optional values into real values that are required for pandoc to run
/// properly and convert files for the given file type conversion.
@_spi(Internal)
public struct EnsuredPandocOptions: Equatable, Sendable {
public static let latexFilename = "Report.tex"
public let buildDirectory: String
public let extraOptions: [String]?
public let files: [String]
@@ -88,7 +146,9 @@ public struct EnsuredPandocOptions: Equatable, Sendable {
public let outputFileType: PandocClient.FileType
public let pdfEngine: String?
public var ensuredExtensionFileName: String {
/// Ensures the output filename includes the file extension, so that pandoc
/// can properly convert the files.
public var ensuredFilename: String {
let extensionString = ".\(outputFileType.fileExtension)"
if !outputFileName.hasSuffix(extensionString) {
@@ -97,14 +157,33 @@ public struct EnsuredPandocOptions: Equatable, Sendable {
return outputFileName
}
/// Generates the options required for building the latex file that is needed
/// to convert the project to an html output file.
var preHtmlLatexOptions: Self {
.init(
buildDirectory: buildDirectory,
extraOptions: extraOptions,
files: files,
includeInHeader: includeInHeader,
outputFileName: Self.latexFilename,
outputFileType: .latex,
pdfEngine: nil
)
}
/// Generate arguments for the pandoc shell command based on the parsed options
/// for a given conversion.
///
func makeArguments(
outputPath: String,
projectDirectory: String
) -> [String] {
var arguments = [PandocClient.Constants.pandocCommand]
arguments += includeInHeader.map {
"--include-in-header=\(projectDirectory)/\(buildDirectory)/\($0)"
if outputFileType != .html {
arguments += includeInHeader.map {
"--include-in-header=\(projectDirectory)/\(buildDirectory)/\($0)"
}
}
if let pdfEngine {
@@ -117,8 +196,12 @@ public struct EnsuredPandocOptions: Equatable, Sendable {
arguments.append(contentsOf: extraOptions)
}
arguments += files.map {
"\(projectDirectory)/\(buildDirectory)/\($0)"
if outputFileType != .html {
arguments += files.map {
"\(projectDirectory)/\(buildDirectory)/\($0)"
}
} else {
arguments.append("\(projectDirectory)/\(buildDirectory)/\(Self.latexFilename)")
}
return arguments
@@ -145,15 +228,15 @@ public func ensurePandocOptions(
}
@_spi(Internal)
public extension PandocClient.FileType {
func parsePdfEngine(
extension PandocClient.FileType {
public func parsePdfEngine(
_ configuration: Configuration.Generate?,
_ defaults: Configuration.Generate
) -> String? {
switch self {
case .html, .latex:
return nil
case let .pdf(engine: engine):
case .pdf(let engine):
if let engine {
return engine
} else if let engine = configuration?.pdfEngine {
@@ -168,8 +251,8 @@ public extension PandocClient.FileType {
}
@_spi(Internal)
public extension PandocClient.RunOptions {
func parseFiles(
extension PandocClient.RunOptions {
public func parseFiles(
_ configuration: Configuration.Generate?,
_ defaults: Configuration.Generate
) -> [String] {
@@ -187,7 +270,7 @@ public extension PandocClient.RunOptions {
}
}
func parseIncludeInHeader(
public func parseIncludeInHeader(
_ configuration: Configuration.Generate?,
_ defaults: Configuration.Generate
) -> [String] {
@@ -205,7 +288,7 @@ public extension PandocClient.RunOptions {
}
}
func parseOutputFileName(
public func parseOutputFileName(
_ configuration: Configuration.Generate?,
_ defaults: Configuration.Generate
) -> String {
@@ -223,7 +306,7 @@ public extension PandocClient.RunOptions {
}
}
func parseBuildDirectory(
public func parseBuildDirectory(
_ configuration: Configuration.Generate?,
_ defaults: Configuration.Generate
) -> String {

View File

@@ -4,14 +4,14 @@ import Dependencies
import DependenciesMacros
import Foundation
public extension DependencyValues {
extension DependencyValues {
/// Represents interactions with the `pandoc` command line application.
///
/// The `pandoc` command line application is used to generate the final output
/// documents from a home performance assessment project.
///
var pandocClient: PandocClient {
public var pandocClient: PandocClient {
get { self[PandocClient.self] }
set { self[PandocClient.self] = newValue }
}

View File

@@ -81,6 +81,8 @@ extension PlaybookClient.RunPlaybook.SharedRunOptions {
) async throws -> T {
@Dependency(\.commandClient) var commandClient
try await ensurePlaybookExists()
return try await commandClient.run(
logging: loggingOptions,
quiet: quiet,
@@ -101,6 +103,17 @@ extension PlaybookClient.RunPlaybook.SharedRunOptions {
return (arguments, output)
}
}
private func ensurePlaybookExists() async throws {
@Dependency(\.fileClient) var fileClient
@Dependency(\.playbookClient.repository) var repository
let directory = try await repository.directory()
let exists = try await fileClient.isDirectory(URL(filePath: directory))
if !exists {
try await repository.install()
}
}
}
@_spi(Internal)

7
TODO.md Normal file
View File

@@ -0,0 +1,7 @@
# TODO
- [ ] Build docker images in ci.
- [ ] Generate documentation for docker usage.
- [ ] Generally need to create a local wrapper script to mount volumes.
- [ ] Completions can be installed / used with the wrapper script by calling
`docker run --it --rm <image> --generate-completion-script <shell> > /path/to/completions/on/local`

View File

@@ -23,7 +23,11 @@ struct FileClientTests {
let fileClient = FileClient.liveValue
let vaultFilePath = url.appending(path: fileName)
FileManager.default.createFile(atPath: vaultFilePath.cleanFilePath, contents: nil)
#if os(Linux)
_ = FileManager.default.createFile(atPath: vaultFilePath.cleanFilePath, contents: nil)
#else
FileManager.default.createFile(atPath: vaultFilePath.cleanFilePath, contents: nil)
#endif
let output = try await fileClient.findVaultFile(url)!
#expect(output.cleanFilePath == vaultFilePath.cleanFilePath)
@@ -43,7 +47,11 @@ struct FileClientTests {
try await fileClient.createDirectory(subDir)
let vaultFilePath = subDir.appending(path: fileName)
FileManager.default.createFile(atPath: vaultFilePath.cleanFilePath, contents: nil)
#if os(Linux)
_ = FileManager.default.createFile(atPath: vaultFilePath.cleanFilePath, contents: nil)
#else
FileManager.default.createFile(atPath: vaultFilePath.cleanFilePath, contents: nil)
#endif
let output = try await fileClient.findVaultFile(url)!
#expect(output.cleanFilePath == vaultFilePath.cleanFilePath)

View File

@@ -1,8 +1,8 @@
@_spi(Internal) import ConfigurationClient
@_spi(Internal) import PandocClient
import PlaybookClient
import Testing
import TestSupport
import Testing
@Suite("PandocClientTests")
struct PandocClientTests: TestCase {
@@ -13,12 +13,12 @@ struct PandocClientTests: TestCase {
static let expectedIncludeInHeaders = [
"--include-in-header=/project/.build/head.tex",
"--include-in-header=/project/.build/footer.tex"
"--include-in-header=/project/.build/footer.tex",
]
static let expectedFiles = [
"/project/.build/Report.md",
"/project/.build/Definitions.md"
"/project/.build/Definitions.md",
]
static var sharedRunOptions: PandocClient.RunOptions {
@@ -49,7 +49,8 @@ struct PandocClientTests: TestCase {
#expect(output == "\(Self.outputDirectory)/\(Self.defaultFileName).tex")
} assert: { output in
let expected = ["pandoc"]
let expected =
["pandoc"]
+ Self.expectedIncludeInHeaders
+ ["--output=\(Self.outputDirectory)/\(Self.defaultFileName).tex"]
+ Self.expectedFiles
@@ -71,10 +72,11 @@ struct PandocClientTests: TestCase {
#expect(output == "\(Self.outputDirectory)/\(Self.defaultFileName).html")
} assert: { output in
let expected = ["pandoc"]
+ Self.expectedIncludeInHeaders
+ ["--output=\(Self.outputDirectory)/\(Self.defaultFileName).html"]
+ Self.expectedFiles
let expected = [
"pandoc",
"--output=\(Self.outputDirectory)/\(Self.defaultFileName).html",
"\(Self.projectDirectory)/.build/Report.tex",
]
#expect(output.arguments == expected)
}
@@ -83,7 +85,7 @@ struct PandocClientTests: TestCase {
@Test(
arguments: [
nil,
"lualatex"
"lualatex",
]
)
func generatePdf(pdfEngine: String?) async throws {
@@ -94,11 +96,13 @@ struct PandocClientTests: TestCase {
} run: {
@Dependency(\.pandocClient) var pandocClient
let output = try await pandocClient.run.generatePdf(Self.sharedRunOptions, pdfEngine: pdfEngine)
let output = try await pandocClient.run.generatePdf(
Self.sharedRunOptions, pdfEngine: pdfEngine)
#expect(output == "\(Self.outputDirectory)/\(Self.defaultFileName).pdf")
} assert: { output in
let expected = ["pandoc"]
let expected =
["pandoc"]
+ Self.expectedIncludeInHeaders
+ ["--pdf-engine=\(pdfEngine ?? "xelatex")"]
+ ["--output=\(Self.outputDirectory)/\(Self.defaultFileName).pdf"]
@@ -147,10 +151,18 @@ struct TestPdfEngine: Sendable {
static let testCases: [Self] = [
.init(fileType: .html, expectedEngine: nil, configuration: .init(), defaults: .default),
.init(fileType: .latex, expectedEngine: nil, configuration: .init(), defaults: .default),
.init(fileType: .pdf(engine: "lualatex"), expectedEngine: "lualatex", configuration: .init(), defaults: .default),
.init(fileType: .pdf(engine: nil), expectedEngine: "xelatex", configuration: .init(), defaults: .default),
.init(fileType: .pdf(engine: nil), expectedEngine: "xelatex", configuration: .init(), defaults: .init()),
.init(fileType: .pdf(engine: nil), expectedEngine: "xelatex", configuration: .init(generate: .default), defaults: .init())
.init(
fileType: .pdf(engine: "lualatex"), expectedEngine: "lualatex", configuration: .init(),
defaults: .default),
.init(
fileType: .pdf(engine: nil), expectedEngine: "xelatex", configuration: .init(),
defaults: .default),
.init(
fileType: .pdf(engine: nil), expectedEngine: "xelatex", configuration: .init(),
defaults: .init()),
.init(
fileType: .pdf(engine: nil), expectedEngine: "xelatex",
configuration: .init(generate: .default), defaults: .init()),
]
}
@@ -174,19 +186,23 @@ struct TestParseFiles: Sendable {
}
var parsedFiles: [String] {
let runOptions = self.runOptions ?? PandocClient.RunOptions(
loggingOptions: .init(commandName: "parseFiles", logLevel: .debug),
projectDirectory: nil,
quiet: true,
shouldBuild: false
)
let runOptions =
self.runOptions
?? PandocClient.RunOptions(
loggingOptions: .init(commandName: "parseFiles", logLevel: .debug),
projectDirectory: nil,
quiet: true,
shouldBuild: false
)
return runOptions.parseFiles(configuration.generate, defaults)
}
static let testCases: [Self] = [
.init(expectedFiles: ["Report.md", "Definitions.md"]),
.init(expectedFiles: ["Report.md", "Definitions.md"], configuration: .init(generate: .default), defaults: .init()),
.init(
expectedFiles: ["Report.md", "Definitions.md"], configuration: .init(generate: .default),
defaults: .init()),
.init(expectedFiles: [], defaults: .init()),
.init(
expectedFiles: ["custom.md"],
@@ -199,7 +215,7 @@ struct TestParseFiles: Sendable {
quiet: true,
shouldBuild: false
)
)
),
]
}
@@ -223,16 +239,20 @@ struct TestParseIncludeInHeaderFiles: Sendable {
}
var parsedFiles: [String] {
let runOptions = self.runOptions ?? PandocClient.RunOptions(
loggingOptions: .init(commandName: "parseFiles", logLevel: .debug)
)
let runOptions =
self.runOptions
?? PandocClient.RunOptions(
loggingOptions: .init(commandName: "parseFiles", logLevel: .debug)
)
return runOptions.parseIncludeInHeader(configuration.generate, defaults)
}
static let testCases: [Self] = [
.init(expectedHeaderFiles: ["head.tex", "footer.tex"]),
.init(expectedHeaderFiles: ["head.tex", "footer.tex"], configuration: .init(generate: .default), defaults: .init()),
.init(
expectedHeaderFiles: ["head.tex", "footer.tex"], configuration: .init(generate: .default),
defaults: .init()),
.init(expectedHeaderFiles: [], defaults: .init()),
.init(
expectedHeaderFiles: ["custom.tex"],
@@ -242,7 +262,7 @@ struct TestParseIncludeInHeaderFiles: Sendable {
loggingOptions: .init(commandName: "parseFiles", logLevel: .debug),
includeInHeader: ["custom.tex"]
)
)
),
]
}
@@ -266,9 +286,11 @@ struct TestParseOutputFileName: Sendable {
}
var parsedFileName: String {
let runOptions = self.runOptions ?? PandocClient.RunOptions(
loggingOptions: .init(commandName: "parseFiles", logLevel: .debug)
)
let runOptions =
self.runOptions
?? PandocClient.RunOptions(
loggingOptions: .init(commandName: "parseFiles", logLevel: .debug)
)
return runOptions.parseOutputFileName(configuration.generate, defaults)
}
@@ -285,7 +307,7 @@ struct TestParseOutputFileName: Sendable {
loggingOptions: .init(commandName: "parseFiles", logLevel: .debug),
outputFileName: "custom"
)
)
),
]
}
@@ -309,9 +331,11 @@ struct TestParseBuildDirectory: Sendable {
}
var parsedBuildDirectory: String {
let runOptions = self.runOptions ?? PandocClient.RunOptions(
loggingOptions: .init(commandName: "parseFiles", logLevel: .debug)
)
let runOptions =
self.runOptions
?? PandocClient.RunOptions(
loggingOptions: .init(commandName: "parseFiles", logLevel: .debug)
)
return runOptions.parseBuildDirectory(configuration.generate, defaults)
}
@@ -328,6 +352,6 @@ struct TestParseBuildDirectory: Sendable {
buildDirectory: "custom",
loggingOptions: .init(commandName: "parseFiles", logLevel: .debug)
)
)
),
]
}

View File

@@ -145,6 +145,7 @@ struct PlaybookClientTests: TestCase {
@Test
func generateTemplate() async throws {
try await withCapturingCommandClient("generateTemplate") {
$0.fileClient.isDirectory = { _ in true }
$0.configurationClient = .mock()
$0.playbookClient = .liveValue
} run: {
@@ -180,6 +181,7 @@ struct PlaybookClientTests: TestCase {
operation: @Sendable @escaping () async throws -> Void
) async rethrows {
try await withDependencies {
$0.fileClient.isDirectory = { _ in true }
$0.configurationClient = .mock(configuration)
$0.commandClient = .capturing(capturing)
$0.playbookClient = .liveValue

View File

@@ -2,23 +2,76 @@
# Build the executable
ARG SWIFT_IMAGE_VERSION="6.0.3"
FROM swift:${SWIFT_IMAGE_VERSION} AS build
# ============================================================
# Build Swift Image
# ============================================================
FROM docker.io/swift:${SWIFT_IMAGE_VERSION} AS build
# Install OS updates
RUN export DEBIAN_FRONTEND=nointeractive DEBCONF_NOINTERACTIVE_SEEN=true && \
apt-get -q update && \
apt-get -q dist-upgrade -y && \
apt-get install -y libjemalloc-dev
WORKDIR /build
# Resolve dependencies, this creates a cached layer.
COPY ./Package.* ./
RUN swift package resolve
RUN --mount=type=cache,target=/build/.build swift package resolve
COPY . .
RUN swift build -c release -Xswiftc -g
# Run image
FROM swift:${SWIFT_IMAGE_VERSION}-slim
# Build the application.
RUN --mount=type=cache,target=/build/.build \
swift build -c release \
--product hpa \
--static-swift-stdlib \
-Xlinker -ljemalloc
RUN export DEBIAN_FRONTEND=nointeractive DEBCONF_NOINTERACTIVE_SEEN=true && apt-get -q update && \
# Switch to staging area.
WORKDIR /staging
# Copy main executable to staging area.
RUN --mount=type=cache,target=/build/.build \
cp "$(swift build --package-path /build -c release --show-bin-path)/hpa" ./
# ============================================================
# Run Image
# ============================================================
FROM docker.io/ubuntu:noble
# Update base image and install needed packages.
#
# NOTE: NB: Installs vim as minimal text editor to use inside the container, bc
# when I mount my home directory / use my neovim config it requires
# neovim v11+, but generally only going to edit ansible vault files
# inside the container.
RUN export DEBIAN_FRONTEND=nointeractive DEBCONF_NOINTERACTIVE_SEEN=true && \
apt-get -q update && \
apt-get -q dist-upgrade -y && \
apt-get -q install -y \
ansible \
curl \
imagemagick \
pandoc \
texlive \
texlive-xetex \
libjemalloc2 \
libcurl4 \
tzdata \
vim \
&& rm -r /var/lib/apt/lists/*
COPY --from=build /build/.build/release/hpa /usr/local/bin
CMD ["/bin/bash", "-xc", "/usr/local/bin/hpa"]
# Install the hpa executable.
COPY --from=build /staging/hpa /usr/local/bin
# Install the entrypoint script and make execuatable.
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh && mkdir /root/project
# Set workdir and volume mounts.
WORKDIR /root/project
VOLUME /root/project
ENTRYPOINT [ "/entrypoint.sh" ]
CMD ["--help"]

27
docker/entrypoint.sh Normal file
View File

@@ -0,0 +1,27 @@
#!/bin/bash
declare -a args
# Allows to attach to a shell inside the container, or run ansbile commands,
# otherwise run the 'hpa' script with the given arguments.
#
while [[ $# -gt 0 ]]; do
if [[ $1 == "/bin/bash" ]] || [[ $1 == "bash" ]]; then
shift
/bin/bash "$@"
exit $?
elif [[ $1 == "/bin/sh" ]] || [[ $1 == "sh" ]]; then
shift
/bin/sh "$@"
exit $?
elif [[ $1 =~ ^ansible ]]; then
exec "$@"
exit $?
else
args+=("$1")
fi
shift
done
# If we made it here then run the hpa script.
/usr/local/bin/hpa "${args[@]}"

View File

@@ -52,6 +52,8 @@ test-docker *ARGS: (build-docker-test)
{{docker_image_name}}:test \
swift test {{ARGS}}
alias td := test-docker
# Remove bottles
remove-bottles:
rm -rf *.gz