diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index ddf9963..0911e9a 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -2,7 +2,7 @@ name: CI on: push: - branches: ["main", "dev"] + branches: ["main"] pull_request: jobs: diff --git a/.gitea/workflows/docker.yaml b/.gitea/workflows/docker.yaml new file mode 100644 index 0000000..0b637d8 --- /dev/null +++ b/.gitea/workflows/docker.yaml @@ -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 }} diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml index 5a638f9..d9b7369 100644 --- a/.gitea/workflows/release.yaml +++ b/.gitea/workflows/release.yaml @@ -15,5 +15,3 @@ jobs: uses: actions/checkout@v4 - name: Release uses: softprops/action-gh-release@v2 - with: - draft: true diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..debb57b --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2024 Michael Housh + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c476a7c --- /dev/null +++ b/README.md @@ -0,0 +1,179 @@ +# swift-hpa + +A command-line application for managing home performance assessment projects from user defined +template repositories. + +This tool is a wrapper around several other command line applications, the primary ones being: + +1. `ansible-playbook` +1. `ansible-vault` +1. `pandoc` + +## Installation + +You can install the application using homebrew. + +```bash +brew tap michael/formula https://git.housh.dev/michael/homebrew-formula +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. + +This application requires some dependencies to be installed on your system, you can install the +dependencies with the following command. + +```bash +hpa utils install-dependencies +``` + +The dependencies installed are: + +1. ansible +1. imagemagick +1. pandoc +1. texLive + +It will also download an ansible-playbook that is used to generate output files, template +repositories, and encrypt / decrypt variable files. The playbook get's installed to +`~/.local/share/hpa/playbook`. + +> 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. + +When you first download the application you can setup the configuration file for your use case. + +```bash +hpa utils generate-config +``` + +This will create a configuration file in the default location: `~/.config/hpa/config.toml`, which +can be edited to suit your needs. + +## Getting Started + +The first step to getting started is creating your template. This is used to create projects. The +template defines the structure of a project and defines variables which are used to generate the +final output files of a project. + +You can generate the template using following command: + +```bash +hpa utils generate-template --path ~/projects/my-template +``` + +Where the `--path` is where you would like the template to be on your local system. + +It is recommended that after you get your template setup to your liking that you turn it into a +`git` repository. Therefore your projects can be pinned to specific version of the template. This +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. + +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. + +```bash +hpa create ~/consults/my-first-consult +``` + +The above assumes that your template is a `git` repository and that your configuration is setup +properly. If you want to experiment with a local template that is on your system then you can you +can use one of the following command options. + +```bash +hpa create --template-dir ~/projects/my-template ~/consults/my-first-consult +``` + +Or if your configuration has `directory` set in the `template` section. + +```bash +hpa create --use-local-template ~/consults/my-first-consult +``` + +## 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. + +```bash +hpa generate pdf +``` + +The above _assumes_ that you are inside your project directory, if you would like to generate an +output file from outside of your project directory you can specify the path to the project you would +like to generate output for. + +```bash +hpa generate pdf --project-directory ~/consults/my-first-consult +``` + +Currently the supported output file types are: + +1. PDF +1. LaTeX +1. HTML + +## 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 +your project directory or in your template directory. It will decrypt any sensitive data stored in +`vault` files as well. + +These files get placed inside a directory in the project, default location is `.build`. The generate +commands by default build the project for you, unless you specify the `--no-build` option. + +You can explore the contents of the `.build` directory or if you'd like to separate the build and +generate steps, you can build a project using the following command: + +```bash +hpa build +``` + +The above _assumes_ that you are inside your project directory, if you would like to generate an +output file from outside of your project directory you can specify the path to the project you would +like to generate output for. + +```bash +hpa build --project-directory ~/consults/my-first-consult +``` + +## 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 +which will suppress logging output and allow piping into other commands. + +Along the similar line, if you would like to increase the logging output then all commands accept +`-v | --verbose` that will increase the logging output. This can be passed multiple times, so for +the highest log output you can do `-vvv`. + +## Uninstalling + +You can uninstall the application using: + +```bash +brew uninstall hpa +``` + +Also remove the configuration and playbook directories. + +```bash +rm -rf ~/.config/hpa +rm -rf ~/.local/share/hpa +``` + +## LICENSE + +This project is licensed under the `MIT` license. + +[See license](https://git.housh.dev/michael/swift-hpa/LICENSE) diff --git a/Release.md b/Release.md new file mode 100644 index 0000000..756dbdc --- /dev/null +++ b/Release.md @@ -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` diff --git a/Sources/FileClient/FileClient.swift b/Sources/FileClient/FileClient.swift index 6619b8a..abf17b2 100644 --- a/Sources/FileClient/FileClient.swift +++ b/Sources/FileClient/FileClient.swift @@ -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 } diff --git a/Sources/PlaybookClient/PlaybookClient+RunPlaybook.swift b/Sources/PlaybookClient/PlaybookClient+RunPlaybook.swift index 6bdd7d9..ba303f0 100644 --- a/Sources/PlaybookClient/PlaybookClient+RunPlaybook.swift +++ b/Sources/PlaybookClient/PlaybookClient+RunPlaybook.swift @@ -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) diff --git a/Sources/hpa/CreateCommand.swift b/Sources/hpa/CreateCommand.swift index d7931d8..a0b0b81 100644 --- a/Sources/hpa/CreateCommand.swift +++ b/Sources/hpa/CreateCommand.swift @@ -40,7 +40,10 @@ struct CreateCommand: AsyncParsableCommand { var templateDir: String? @Flag( - name: .shortAndLong, + name: [ + .short, + .customLong("use-local-template") + ], help: "Force using a local template directory." ) var localTemplateDir = false diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..7b491c1 --- /dev/null +++ b/TODO.md @@ -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 --generate-completion-script > /path/to/completions/on/local` diff --git a/Tests/FileClientTests/FileClientTests.swift b/Tests/FileClientTests/FileClientTests.swift index 2b77fa3..9cdaad4 100644 --- a/Tests/FileClientTests/FileClientTests.swift +++ b/Tests/FileClientTests/FileClientTests.swift @@ -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) diff --git a/Tests/PlaybookClientTests/PlaybookClientTests.swift b/Tests/PlaybookClientTests/PlaybookClientTests.swift index ae55130..db2eabd 100644 --- a/Tests/PlaybookClientTests/PlaybookClientTests.swift +++ b/Tests/PlaybookClientTests/PlaybookClientTests.swift @@ -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 diff --git a/docker/Dockerfile b/docker/Dockerfile index 47e69fa..a08ac1d 100755 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -2,23 +2,70 @@ # 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 + +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 \ + libjemalloc2 \ + libcurl4 \ + tzdata \ && rm -r /var/lib/apt/lists/* -COPY --from=build /build/.build/release/hpa /usr/local/bin -CMD ["/bin/bash", "-xc", "/usr/local/bin/hpa"] +COPY --from=build /staging/hpa /usr/local/bin + +# Setup volumes +RUN mkdir /config && \ + mkdir /consults && \ + mkdir /playbook && \ + mkdir /template && \ + mkdir -p /root/.local/share/hpa && \ + ln -sfv /config /root/.config && \ + ln -sfv /playbook /root/.local/share/hpa/playbook + +VOLUME /config /consults /playbook /template + +ENTRYPOINT [ "/usr/local/bin/hpa" ] +CMD ["--help"] diff --git a/justfile b/justfile index fbba002..0911b22 100644 --- a/justfile +++ b/justfile @@ -6,6 +6,8 @@ tap_url := "https://git.housh.dev/michael/homebrew-formula" tap := "michael/formula" formula := "hpa" +release_base_url := "https://git.housh.dev/michael/swift-hpa/archive" + # Build and bottle homebrew formula. bottle: @brew uninstall {{formula}} || true @@ -50,6 +52,12 @@ test-docker *ARGS: (build-docker-test) {{docker_image_name}}:test \ swift test {{ARGS}} +alias td := test-docker + +# Remove bottles +remove-bottles: + rm -rf *.gz + # Run the application. run *ARGS: swift run hpa {{ARGS}} @@ -67,3 +75,12 @@ update-version: --allow-writing-to-package-directory \ update-version \ hpa + +# 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" && \ + sha=$(curl "$url" | shasum -a 256) && \ + stripped="${sha% *}" && \ + echo "$stripped" | pbcopy && \ + echo "Copied sha to clipboard: $stripped"