Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
name: Build and Test

on:
push:
branches: [ main, 'claude/**' ]
pull_request:
branches: [ main ]

jobs:
build-and-test:
name: Build and Test
runs-on: macos-15

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: List available Xcode versions
run: ls -la /Applications/ | grep Xcode

- name: Select Xcode version
run: |
# Use the latest available Xcode
XCODE_PATH=$(ls -d /Applications/Xcode*.app 2>/dev/null | sort -V | tail -1)
echo "Using Xcode at: $XCODE_PATH"
sudo xcode-select -s "$XCODE_PATH/Contents/Developer"

- name: Show Xcode version
run: |
xcodebuild -version
swift --version

- name: Show project info
run: |
xcodebuild -list -project JobScout.xcodeproj

- name: Resolve Swift Package Dependencies
run: |
xcodebuild -resolvePackageDependencies \
-project JobScout.xcodeproj \
-scheme JobScout \
-clonedSourcePackagesDirPath ./SPM

- name: Build
run: |
# Build and capture ALL output including errors
xcodebuild build \
-project JobScout.xcodeproj \
-scheme JobScout \
-configuration Debug \
-destination 'platform=macOS' \
-clonedSourcePackagesDirPath ./SPM \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO \
2>&1 | tee build.log

BUILD_STATUS=${PIPESTATUS[0]}

# Show last 200 lines of build output on failure
if [ $BUILD_STATUS -ne 0 ]; then
echo "========================================"
echo "BUILD FAILED - Last 200 lines of log:"
echo "========================================"
tail -200 build.log
echo ""
echo "========================================"
echo "All error: lines:"
echo "========================================"
grep -i "error:" build.log || echo "No error: lines found"
echo ""
echo "========================================"
echo "All warning: lines:"
echo "========================================"
grep -i "warning:" build.log | tail -50 || echo "No warning: lines found"
exit $BUILD_STATUS
fi

- name: Run Tests
run: |
xcodebuild test \
-project JobScout.xcodeproj \
-scheme JobScout \
-configuration Debug \
-destination 'platform=macOS' \
-clonedSourcePackagesDirPath ./SPM \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO \
2>&1 | tee test.log

- name: Upload build log on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: build-logs
path: |
build.log
test.log
retention-days: 7
4 changes: 2 additions & 2 deletions JobScout.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -636,15 +636,15 @@
/* Begin XCRemoteSwiftPackageReference section */
493E1D5C2EFF830C00DEE268 /* XCRemoteSwiftPackageReference "html2md" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "git@github.com:tim-gromeyer/html2md.git";
repositoryURL = "https://github.com/tim-gromeyer/html2md.git";
requirement = {
branch = main;
kind = branch;
};
};
493E1D5F2EFF869300DEE268 /* XCRemoteSwiftPackageReference "SwiftAgents" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "git@github.com:iliasaz/SwiftAgents.git";
repositoryURL = "https://github.com/iliasaz/SwiftAgents.git";
requirement = {
branch = main;
kind = branch;
Expand Down
62 changes: 62 additions & 0 deletions JobScout/Database/DatabaseManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,68 @@ actor DatabaseManager {
""")
}

// Migration 10: Add user_resume table for storing uploaded resume PDFs
migrator.registerMigration("010_AddUserResume") { db in
try db.execute(sql: """
CREATE TABLE IF NOT EXISTS "user_resume" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"file_name" TEXT NOT NULL,
"pdf_data" BLOB NOT NULL,
"file_size" INTEGER NOT NULL,
"uploaded_at" TEXT NOT NULL DEFAULT (datetime('now')),
"created_at" TEXT NOT NULL DEFAULT (datetime('now')),
"updated_at" TEXT NOT NULL DEFAULT (datetime('now'))
)
""")
}

// Migration 11: Add extracted_text column to user_resume table
migrator.registerMigration("011_AddResumeExtractedText") { db in
// Check if column already exists
let columns = try Row.fetchAll(db, sql: "PRAGMA table_info(user_resume)")
let columnNames = columns.map { $0["name"] as String }

if !columnNames.contains("extracted_text") {
try db.execute(sql: """
ALTER TABLE user_resume ADD COLUMN "extracted_text" TEXT
""")
}

if !columnNames.contains("extraction_status") {
try db.execute(sql: """
ALTER TABLE user_resume ADD COLUMN "extraction_status" TEXT DEFAULT 'pending'
""")
}

if !columnNames.contains("extraction_error") {
try db.execute(sql: """
ALTER TABLE user_resume ADD COLUMN "extraction_error" TEXT
""")
}
}

// Migration 12: Add resume_chunks table for storing chunked text
migrator.registerMigration("012_AddResumeChunks") { db in
try db.execute(sql: """
CREATE TABLE IF NOT EXISTS "resume_chunks" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"resume_id" INTEGER NOT NULL,
"chunk_index" INTEGER NOT NULL,
"content" TEXT NOT NULL,
"character_count" INTEGER NOT NULL,
"word_count" INTEGER NOT NULL,
"created_at" TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (resume_id) REFERENCES user_resume(id) ON DELETE CASCADE,
UNIQUE(resume_id, chunk_index)
)
""")

// Create index for efficient chunk retrieval
try db.execute(sql: """
CREATE INDEX IF NOT EXISTS idx_resume_chunks_resume_id ON resume_chunks(resume_id)
""")
}

// Run all migrations
try migrator.migrate(dbQueue)
}
Expand Down
88 changes: 88 additions & 0 deletions JobScout/Models/UserResume.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//
// UserResume.swift
// JobScout
//
// Created by Claude on 1/11/26.
//

import Foundation
import GRDB

/// Status of text extraction from the resume PDF
enum ExtractionStatus: String, Sendable, Codable {
case pending = "pending"
case processing = "processing"
case completed = "completed"
case failed = "failed"
}

/// Represents a user's uploaded resume stored in the database
struct UserResume: Identifiable, Sendable {
let id: Int
let fileName: String
let pdfData: Data
let fileSize: Int
let uploadedAt: Date
let createdAt: Date
let updatedAt: Date

// Text extraction fields
let extractedText: String?
let extractionStatus: ExtractionStatus
let extractionError: String?

/// Formatted file size for display (e.g., "1.2 MB")
var formattedFileSize: String {
let formatter = ByteCountFormatter()
formatter.countStyle = .file
return formatter.string(fromByteCount: Int64(fileSize))
}

/// Whether text has been successfully extracted
var hasExtractedText: Bool {
extractionStatus == .completed && extractedText != nil && !extractedText!.isEmpty
}

/// Create from database row
static func from(row: Row) -> UserResume {
let statusString: String = row["extraction_status"] ?? "pending"
let extractionStatus = ExtractionStatus(rawValue: statusString) ?? .pending

return UserResume(
id: row["id"],
fileName: row["file_name"],
pdfData: row["pdf_data"],
fileSize: row["file_size"],
uploadedAt: row["uploaded_at"],
createdAt: row["created_at"],
updatedAt: row["updated_at"],
extractedText: row["extracted_text"],
extractionStatus: extractionStatus,
extractionError: row["extraction_error"]
)
}
}

/// Represents a chunk of text extracted from a resume
struct ResumeChunk: Identifiable, Sendable {
let id: Int
let resumeId: Int
let chunkIndex: Int
let content: String
let characterCount: Int
let wordCount: Int
let createdAt: Date

/// Create from database row
static func from(row: Row) -> ResumeChunk {
ResumeChunk(
id: row["id"],
resumeId: row["resume_id"],
chunkIndex: row["chunk_index"],
content: row["content"],
characterCount: row["character_count"],
wordCount: row["word_count"],
createdAt: row["created_at"]
)
}
}
Loading
Loading