feat: Sets up ci workflows
This commit is contained in:
20
.gitea/workflows/ci.yml
Normal file
20
.gitea/workflows/ci.yml
Normal 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
|
||||
63
.gitea/workflows/release.yml
Normal file
63
.gitea/workflows/release.yml
Normal 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 }}
|
||||
@@ -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
|
||||
@@ -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() {}
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
32
dev.Dockerfile
Normal 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"]
|
||||
Reference in New Issue
Block a user