Compare commits

..
Author SHA1 Message Date
Fedor Korotkov 02bf5651e7 tart clone: make pruning limit configurable (#1126)
* tart clone: make pruning limit configurable

* Fixed compilation
2025-09-14 12:38:57 -04:00
Fedor Korotkov 96c89ad76e tart clone: cap automatic pruning at 100 GB (#1124) 2025-09-14 09:40:58 -04:00
Nikolay Edigaryev b78fa6ba1c ASIF is available only starting from macOS 26 (Tahoe) (#1096)
* ASIF is available only starting from macOS 26 (Tahoe)

* Remove testRawFormatIsAlwaysSupported() test

* Fix testASIFFormatSupport() test to check for macOS 26+
2025-09-14 09:40:06 -04:00
Nikolay Edigaryev e443cfa9a2 tart exec: do not attempt to call TTY-related methods when no -t is set (#1122) 2025-09-12 19:17:17 +04:00
Nikolay Edigaryev e35c13425e tart exec: handle input redirection of regular files (#1106) 2025-07-14 19:49:12 +04:00
7 changed files with 69 additions and 163 deletions
-78
View File
@@ -1,78 +0,0 @@
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"
jobs:
claude-review:
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@beta
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
# model: "claude-opus-4-20250514"
# Direct prompt for automated review (no @claude mention needed)
direct_prompt: |
Please review this pull request and provide feedback on:
- Code quality and best practices
- Potential bugs or issues
- Performance considerations
- Security concerns
- Test coverage
Be constructive and helpful in your feedback.
# Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR
# use_sticky_comment: true
# Optional: Customize review based on file types
# direct_prompt: |
# Review this PR focusing on:
# - For TypeScript files: Type safety and proper interface usage
# - For API endpoints: Security, input validation, and error handling
# - For React components: Performance, accessibility, and best practices
# - For tests: Coverage, edge cases, and test quality
# Optional: Different prompts for different authors
# direct_prompt: |
# ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' &&
# 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' ||
# 'Please provide a thorough code review focusing on our coding standards and best practices.' }}
# Optional: Add specific tools for running tests or linting
# allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)"
# Optional: Skip review for certain conditions
# if: |
# !contains(github.event.pull_request.title, '[skip-review]') &&
# !contains(github.event.pull_request.title, '[WIP]')
-64
View File
@@ -1,64 +0,0 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@beta
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
# model: "claude-opus-4-20250514"
# Optional: Customize the trigger phrase (default: @claude)
# trigger_phrase: "/claude"
# Optional: Trigger when specific user is assigned to an issue
# assignee_trigger: "claude-bot"
# Optional: Allow Claude to run specific commands
# allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
# Optional: Add custom instructions for Claude to customize its behavior for your project
# custom_instructions: |
# Follow our coding standards
# Ensure all new code has tests
# Use TypeScript for new files
# Optional: Custom environment variables for Claude
# claude_env: |
# NODE_ENV: test
+7 -2
View File
@@ -31,6 +31,9 @@ struct Clone: AsyncParsableCommand {
@Flag(help: .hidden)
var deduplicate: Bool = false
@Option(help: ArgumentHelp("limit automatic pruning to n gigabytes", valueName: "n"))
var pruneLimit: UInt = 100
func validate() throws {
if newName.contains("/") {
throw ValidationError("<new-name> should be a local name")
@@ -76,8 +79,10 @@ struct Clone: AsyncParsableCommand {
//
// So, once we clone the VM let's try to claim the rest of space for the VM to run without errors.
let unallocatedBytes = try sourceVM.sizeBytes() - sourceVM.allocatedSizeBytes()
if unallocatedBytes > 0 {
try Prune.reclaimIfNeeded(UInt64(unallocatedBytes), sourceVM)
// Avoid reclaiming an excessive amount of disk space.
let reclaimBytes = min(unallocatedBytes, Int(pruneLimit) * 1024 * 1024 * 1024)
if reclaimBytes > 0 {
try Prune.reclaimIfNeeded(UInt64(reclaimBytes), sourceVM)
}
}, onCancel: {
try? FileManager.default.removeItem(at: tmpVMDir.baseURL)
+57 -12
View File
@@ -87,11 +87,13 @@ struct Exec: AsyncParsableCommand {
$0.args = Array(command.dropFirst(1))
$0.interactive = interactive
$0.tty = tty
$0.terminalSize = .with {
let (width, height) = try! Term.GetSize()
if tty {
$0.terminalSize = .with {
let (width, height) = try! Term.GetSize()
$0.cols = UInt32(width)
$0.rows = UInt32(height)
$0.cols = UInt32(width)
$0.rows = UInt32(height)
}
}
})
})
@@ -100,28 +102,61 @@ struct Exec: AsyncParsableCommand {
try await withThrowingTaskGroup { group in
// Stream host's standard input if interactive mode is enabled
if interactive {
let stdinStream = AsyncStream<Data> { continuation in
let stdinStream = AsyncThrowingStream<Data, Error> { continuation in
let handle = FileHandle.standardInput
handle.readabilityHandler = { handle in
let data = handle.availableData
if isRegularFile(handle.fileDescriptor) {
// Standard input can be a regular file when input redirection (<) is used,
// in which case the handle won't receive any new readability events, so we
// just read the file normally here in chunks and consider done with it
//
// Ideally this is best handled by using non-blocking I/O, but Swift's
// standard library only offers inefficient bytes[1] property and SwiftNIO's
// NIOFileSystem doesn't seem to support opening raw file descriptors.
//
// [1]: https://developer.apple.com/documentation/foundation/filehandle/bytes
while true {
do {
let data = try handle.read(upToCount: 64 * 1024)
if let data = data {
continuation.yield(data)
} else {
continuation.finish()
break
}
} catch (let error) {
continuation.finish(throwing: error)
break
}
}
} else {
handle.readabilityHandler = { handle in
let data = handle.availableData
continuation.yield(data)
if data.isEmpty {
continuation.finish()
if data.isEmpty {
continuation.finish()
} else {
continuation.yield(data)
}
}
}
}
group.addTask {
for await stdinData in stdinStream {
for try await stdinData in stdinStream {
try await execCall.requestStream.send(.with {
$0.type = .standardInput(.with {
$0.data = stdinData
})
})
}
// Signal EOF as we're done reading standard input
try await execCall.requestStream.send(.with {
$0.type = .standardInput(.with {
$0.data = Data()
})
})
}
}
@@ -178,3 +213,13 @@ struct Exec: AsyncParsableCommand {
}
}
}
private func isRegularFile(_ fileDescriptor: Int32) -> Bool {
var stat = stat()
if fstat(fileDescriptor, &stat) != 0 {
return false
}
return (stat.st_mode & S_IFMT) == S_IFREG
}
+1 -1
View File
@@ -21,7 +21,7 @@ enum DiskImageFormat: String, CaseIterable, Codable {
case .raw:
return true
case .asif:
if #available(macOS 15, *) {
if #available(macOS 26, *) {
return true
} else {
return false
+2 -6
View File
@@ -2,13 +2,9 @@ import XCTest
@testable import tart
final class DiskImageFormatTests: XCTestCase {
func testRawFormatIsAlwaysSupported() throws {
XCTAssertTrue(DiskImageFormat.raw.isSupported)
}
func testASIFFormatSupport() throws {
// ASIF should be supported on macOS 15+
if #available(macOS 15, *) {
// ASIF should be supported on macOS 26+
if #available(macOS 26, *) {
XCTAssertTrue(DiskImageFormat.asif.isSupported)
} else {
XCTAssertFalse(DiskImageFormat.asif.isSupported)
+2
View File
@@ -166,6 +166,8 @@ Tart does have an analogue of Anka Controller for managing VMs across a cluster
In case there's not enough space to fit the newly pulled or cloned VM image, Tart will remove the least recently accessed VMs from OCI cache and `.ipsw` files from IPSW cache until enough free space is available.
The `tart clone` command limits this automatic pruning to 100 GB by default to avoid removing too many cached items. You can change this limit with the `--prune-limit` option (in gigabytes).
To disable this functionality, set the `TART_NO_AUTO_PRUNE` environment variable either globally:
```shell