feat: Sets up ci workflows

This commit is contained in:
2025-01-24 20:19:43 -05:00
parent 000f8ce16b
commit d1e2f37629
16 changed files with 200 additions and 73 deletions

20
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,20 @@
name: CI
on:
push:
workflow_dispatch:
jobs:
ubuntu:
name: Ubuntu
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Setup just.
uses: https://git.housh.dev/actions/setup-just.git@v1
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
- name: Setup buildx
uses: docker/setup-buildx-action@v3
- name: Run tests.
run: just test-docker

View File

@@ -0,0 +1,63 @@
name: Create and publish a Docker image
# Configures this workflow to run every time a change is pushed to the branch called `release`.
on:
push:
branches: ['main']
tags:
- '*.*.*'
workflow_dispatch:
# Defines two custom environment variables for the workflow. These are used for the Container registry domain,
# and a name for the Docker image that this workflow builds.
env:
REGISTRY: git.housh.dev
IMAGE_NAME: ${{ gitea.repository }}
# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
jobs:
build-and-push-image:
runs-on: ubuntu-latest
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
permissions:
contents: read
packages: write
attestations: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Uses the `docker/login-action` action to log in to the Container registry registry using the account
# and password that will publish the packages. Once published, the packages are scoped to the account defined here.
- name: Log in to the Container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with:
registry: ${{ env.REGISTRY }}
username: ${{ gitea.actor }}
password: ${{ secrets.CONTAINER_TOKEN }}
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels
# that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a
# subsequent step. The `images` value provides the base name for the tags and labels.
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=sha
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If
# the build succeeds, it pushes the image to GitHub Packages. It uses the `context` parameter to define the build's context
# as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)"
# in the README of the `docker/build-push-action` repository.
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
- name: Build and push Docker image
id: push
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
with:
context: .
file: Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -1,28 +0,0 @@
// import Dependencies
// import Foundation
//
// public extension DependencyValues {
// var dateFormatter: DateFormatter {
// get { self[DateFormatter.self] }
// set { self[DateFormatter.self] = newValue }
// }
// }
//
// #if hasFeature(RetroactiveAttribute)
// extension DateFormatter: @retroactive DependencyKey {
//
// public static var liveValue: DateFormatter {
// let formatter = DateFormatter()
// formatter.dateStyle = .short
// return formatter
// }
// }
// #else
// extension DateFormatter: DependencyKey {
// public static var liveValue: DateFormatter {
// let formatter = DateFormatter()
// formatter.dateStyle = .short
// return formatter
// }
// }
// #endif

View File

@@ -127,10 +127,10 @@ final class EmployeeModel: Model, @unchecked Sendable {
@Field(key: "is_active")
var active: Bool
@Timestamp(key: "created_at", on: .create)
@Timestamp(key: "created_at", on: .create, format: .iso8601)
var createdAt: Date?
@Timestamp(key: "updated_at", on: .update)
@Timestamp(key: "updated_at", on: .update, format: .iso8601)
var updatedAt: Date?
init() {}

View File

@@ -142,10 +142,10 @@ final class PurchaseOrderModel: Model, Codable, @unchecked Sendable {
@Parent(key: "vendor_branch_id")
var vendorBranch: VendorBranchModel
@Timestamp(key: "created_at", on: .create)
@Timestamp(key: "created_at", on: .create, format: .iso8601)
var createdAt: Date?
@Timestamp(key: "updated_at", on: .update)
@Timestamp(key: "updated_at", on: .update, format: .iso8601)
var updatedAt: Date?
init() {}

View File

@@ -182,10 +182,10 @@ final class UserModel: Model, @unchecked Sendable {
@Field(key: "password_hash")
var passwordHash: String
@Timestamp(key: "created_at", on: .create)
@Timestamp(key: "created_at", on: .create, format: .iso8601)
var createdAt: Date?
@Timestamp(key: "updated_at", on: .update)
@Timestamp(key: "updated_at", on: .update, format: .iso8601)
var updatedAt: Date?
@OptionalChild(for: \.$user)

View File

@@ -125,10 +125,10 @@ final class VendorBranchModel: Model, @unchecked Sendable {
@Field(key: .string(FieldKey.name))
var name: String
@Timestamp(key: .string(FieldKey.createdAt), on: .create)
@Timestamp(key: .string(FieldKey.createdAt), on: .create, format: .iso8601)
var createdAt: Date?
@Timestamp(key: .string(FieldKey.updatedAt), on: .update)
@Timestamp(key: .string(FieldKey.updatedAt), on: .update, format: .iso8601)
var updatedAt: Date?
@Parent(key: .string(FieldKey.vendorID))

View File

@@ -109,10 +109,10 @@ final class VendorModel: Model, @unchecked Sendable {
@Field(key: "name")
var name: String
@Timestamp(key: "created_at", on: .create)
@Timestamp(key: "created_at", on: .create, format: .iso8601)
var createdAt: Date?
@Timestamp(key: "updated_at", on: .update)
@Timestamp(key: "updated_at", on: .update, format: .iso8601)
var updatedAt: Date?
@Children(for: \.$vendor)

View File

@@ -1,28 +1,53 @@
import Dependencies
import DependenciesMacros
import Foundation
public extension DependencyValues {
var dateFormatter: DateFormatter {
get { self[DateFormatter.self] }
set { self[DateFormatter.self] = newValue }
var dateFormatter: DateFormatterKey {
get { self[DateFormatterKey.self] }
set { self[DateFormatterKey.self] = newValue }
}
}
#if hasFeature(RetroactiveAttribute)
extension DateFormatter: @retroactive DependencyKey {
@DependencyClient
public struct DateFormatterKey: Sendable {
public var string: @Sendable (Date?) -> String = { _ in "N/A" }
}
public static var liveValue: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .short
return formatter
}
extension DateFormatterKey: DependencyKey {
public static let testValue = Self()
public static var liveValue: DateFormatterKey {
.init(
string: { date in
guard let date else { return "N/A" }
let formatter = ISO8601DateFormatter()
formatter.timeZone = TimeZone(identifier: "UTC")
formatter.formatOptions = [.withFullDate]
return formatter.string(from: date)
}
)
}
#else
extension DateFormatter: DependencyKey {
public static var liveValue: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .short
return formatter
}
}
#endif
}
// #if hasFeature(RetroactiveAttribute)
// extension DateFormatter: @retroactive DependencyKey {
//
// public static var liveValue: DateFormatter {
// let formatter = DateFormatter()
// formatter.dateStyle = .short
// return formatter
// }
// }
// #else
// extension DateFormatter: DependencyKey {
// public static var liveValue: DateFormatter {
// let formatter = ISO8601DateFormatter()
// formatter.locale = Locale(identifier: "en_US_POSIX")
// // formatter.dateFormat = "mm/dd/yyyy"
// formatter.formatOptions = [.withFullDate]
// formatter.timeZone = TimeZone(identifier: "UTC")
// return formatter
// }
// }
// #endif

View File

@@ -104,11 +104,11 @@ struct PurchaseOrderForm: HTML, Sendable {
if let purchaseOrder, let createdAt = purchaseOrder.createdAt {
div(.class("row")) {
label(.class("label col-2")) { "Created:" }
h3(.class("col-2")) { dateFormatter.string(from: createdAt) }
h3(.class("col-2")) { dateFormatter.string(createdAt) }
if let updatedAt = purchaseOrder.updatedAt {
div(.class("col-1")) {}
label(.class("label col-2")) { "Updated:" }
h3(.class("col-2")) { dateFormatter.string(from: updatedAt) }
h3(.class("col-2")) { dateFormatter.string(updatedAt) }
}
}
}

View File

@@ -25,9 +25,9 @@ struct UserDetail: HTML, Sendable {
}
div(.class("row")) {
span(.class("label col-2")) { "Created:" }
span(.class("date col-4")) { dateFormatter.formattedDate(user.createdAt) }
span(.class("date col-4")) { dateFormatter.string(user.createdAt) }
span(.class("label col-2")) { "Updated:" }
span(.class("date col-4")) { dateFormatter.formattedDate(user.updatedAt) }
span(.class("date col-4")) { dateFormatter.string(user.updatedAt) }
}
div(.class("btn-row user-buttons")) {
button(

View File

@@ -24,6 +24,7 @@ struct ViewControllerTests {
try await withDependencies {
$0.viewController = .liveValue
$0.database.employees = .mock
$0.dateFormatter = .mock
} operation: {
@Dependency(\.viewController) var viewController
@Dependency(\.database) var database
@@ -54,6 +55,7 @@ struct ViewControllerTests {
try await withSnapshotTesting(record: record) {
try await withDependencies {
$0.viewController = .liveValue
$0.dateFormatter = .mock
} operation: {
@Dependency(\.viewController) var viewController
@@ -67,7 +69,7 @@ struct ViewControllerTests {
func purchaseOrderViews() async throws {
try await withSnapshotTesting(record: record) {
try await withDependencies {
$0.dateFormatter = .liveValue
$0.dateFormatter = .mock
$0.database.purchaseOrders = .mock
$0.viewController = .liveValue
} operation: {
@@ -103,7 +105,7 @@ struct ViewControllerTests {
func userViews() async throws {
try await withSnapshotTesting(record: record) {
try await withDependencies {
$0.dateFormatter = .liveValue
$0.dateFormatter = .mock
$0.database.users = .mock
$0.viewController = .liveValue
} operation: {
@@ -132,7 +134,7 @@ struct ViewControllerTests {
func vendorViews() async throws {
try await withSnapshotTesting(record: record) {
try await withDependencies {
$0.dateFormatter = .liveValue
$0.dateFormatter = .mock
$0.database.vendors = .mock
$0.viewController = .liveValue
} operation: {
@@ -161,7 +163,7 @@ struct ViewControllerTests {
func vendorBranchViews() async throws {
try await withSnapshotTesting(record: record) {
try await withDependencies {
$0.dateFormatter = .liveValue
$0.dateFormatter = .mock
$0.database.vendors = .mock
$0.database.vendorBranches = .mock
$0.viewController = .liveValue
@@ -271,7 +273,14 @@ extension DatabaseClient.VendorBranches {
extension Date {
static var mock: Self {
Date(timeIntervalSince1970: 1_234_567_890)
let formatter = ISO8601DateFormatter()
return formatter.date(from: "2025-01-31T02:22:40Z")!
}
}
extension DateFormatterKey {
static var mock: Self {
.init(string: { _ in "01/31/2025" })
}
}
@@ -279,10 +288,10 @@ extension Employee {
static var mock: Self {
Employee(
id: UUID(0),
createdAt: Date(timeIntervalSince1970: 1_234_567_890),
createdAt: .mock,
firstName: "Testy",
lastName: "McTestface",
updatedAt: Date(timeIntervalSince1970: 1_234_567_890)
updatedAt: .mock
)
}
}
@@ -296,10 +305,10 @@ extension Employee.Create {
@Dependency(\.date.now) var now
return .init(
id: UUID(0),
createdAt: Date(timeIntervalSince1970: 1_234_567_890),
createdAt: .mock,
firstName: firstName,
lastName: lastName,
updatedAt: Date(timeIntervalSince1970: 1_234_567_890)
updatedAt: .mock
)
}
}

View File

@@ -24,10 +24,10 @@
</div>
<div class="row">
<label class="label col-2">Created:</label>
<h3 class="col-2">2/13/09</h3>
<h3 class="col-2">01/31/2025</h3>
<div class="col-1"></div>
Updated:<label class="label col-2"></label>
<h3 class="col-2">2/13/09</h3>
<h3 class="col-2">01/31/2025</h3>
</div>
<div class="btn-row">
<button class="btn-primary" type="submit">Update</button>

View File

@@ -9,7 +9,7 @@
Email:<label for="email" class="col-2"><span class="label"></span></label>
<input class="col-4" type="email" id="email" name="email" value="test@example.com" required>
</div>
<div class="row"><span class="label col-2">Created:</span><span class="date col-4"></span><span class="label col-2">Updated:</span><span class="date col-4"></span></div>
<div class="row"><span class="label col-2">Created:</span><span class="date col-4">01/31/2025</span><span class="label col-2">Updated:</span><span class="date col-4">01/31/2025</span></div>
<div class="btn-row user-buttons">
<button type="submit" class="btn-secondary">Update</button>
<button class="danger" hx-delete="/api/v1/users/00000000-0000-0000-0000-000000000000" hx-trigger="click" hx-swap="outerHTML" hx-target="#user-00000000-0000-0000-0000-000000000000" hx-confirm="Are you sure you want to delete this user?" hx-on::after-request="toggleContent('float'); window.location.href='/users';">Delete</button>

32
dev.Dockerfile Normal file
View File

@@ -0,0 +1,32 @@
FROM swift:6.0-noble
# Make sure all system packages are up to date, and install only essential packages.
RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
&& apt-get -q update \
&& apt-get -q dist-upgrade -y \
&& apt-get -q install -y \
libjemalloc2 \
ca-certificates \
tzdata \
# If your app or its dependencies import FoundationNetworking, also install `libcurl4`.
libcurl4 \
# If your app or its dependencies import FoundationXML, also install `libxml2`.
# libxml2 \
sqlite3 \
&& rm -r /var/lib/apt/lists/*
# Set up a build area
WORKDIR /app
# First just resolve dependencies.
# This creates a cached layer that can be reused
# as long as your Package.swift/Package.resolved
# files do not change.
COPY ./Package.* ./
RUN swift package resolve \
$([ -f ./Package.resolved ] && echo "--force-resolved-versions" || true)
# Copy entire repo into container
COPY . .
CMD ["swift", "test"]

View File

@@ -18,3 +18,9 @@ clean:
bootstrap:
cp ./env.example .env
test-docker:
@docker run -it --rm \
-v "${PWD}:/app" \
-w "/app" "swift:6.0-noble" \
swift test