// swiftlint:disable file_length
import ForgejoKit
import Foundation
// swiftlint:disable:next type_body_length
struct Seeder {
let baseURL: String
let serviceName: String
let composeFile: String
private let adminUser = "testadmin"
private let adminPass = "admin1234"
private let testbotUser = "testbot"
private let testbotPass = "testbot1234"
private let readonlyUser = "readonlyuser"
private let readonlyPass = "readonly1234"
func seed() async throws {
let adminClient = ForgejoClient(
serverURL: baseURL, username: adminUser, password: adminPass,
allowSelfSignedCertificates: true,
)
let testbotClient = ForgejoClient(
serverURL: baseURL, username: testbotUser, password: testbotPass,
allowSelfSignedCertificates: true,
)
let start = Date()
let total = 7
func step(_ number: Int, _ label: String) {
let elapsed = Int(Date().timeIntervalSince(start))
print("\n[\(number)/\(total)] \(label) (\(elapsed)s elapsed)")
}
step(1, "Creating admin user")
try await withRetry(maxAttempts: 3, delay: .seconds(2), operation: "create admin user") {
try createAdminUser()
}
step(2, "Creating API users")
try await createAPIUsers(adminClient: adminClient)
step(3, "Creating repositories")
try await createRepositories(adminClient: adminClient)
step(4, "Creating issues, files, and metadata")
try await createIssuesFilesMetadata(adminClient: adminClient, testbotClient: testbotClient)
step(5, "Creating pull requests")
try await createPullRequests(testbotClient: testbotClient)
step(6, "Setting pull request metadata")
try await setPRMetadata(adminClient: adminClient, testbotClient: testbotClient)
step(7, "Creating API token")
try await createAPIToken(adminClient: adminClient)
print("\nSeed data created successfully (\(Int(Date().timeIntervalSince(start)))s total).")
}
// MARK: - Phase 1: Create users
private func createAdminUser() throws {
print("Creating admin user...")
try DockerExec.run(
composeFile: composeFile, service: serviceName,
command: [
"forgejo", "admin", "user", "create", "--admin",
"--username", adminUser,
"--password", adminPass,
"--email", "testadmin@test.local",
"--must-change-password=false",
],
)
}
private func createAPIUsers(adminClient: ForgejoClient) async throws {
let adminService = AdminService(client: adminClient)
print("Creating testbot user...")
_ = try await adminService.createUser(
username: testbotUser, password: testbotPass,
email: "testbot@test.local",
)
print("Creating readonlyuser...")
_ = try await adminService.createUser(
username: readonlyUser, password: readonlyPass,
email: "readonlyuser@test.local",
)
}
// MARK: - Phase 2: Create repositories
private func createRepositories(adminClient: ForgejoClient) async throws {
let repoService = RepositoryService(client: adminClient)
print("Creating repositories...")
for (name, desc) in [
("test-repo", nil as String?),
("test-repo-2", "A second test repository" as String?),
("archived-repo", "An archived repository" as String?),
("html-readme-repo", "Repo with HTML in README" as String?),
] {
_ = try await repoService.createRepository(
name: name, description: desc, autoInit: true,
)
}
print("Repositories created.")
print("Archiving archived-repo...")
try await withRetry(maxAttempts: 5, delay: .seconds(2), operation: "archive repo") {
_ = try await repoService.editRepository(
owner: adminUser, repo: "archived-repo", archived: true,
)
}
// Forgejo 15 enables actions on every new repo by default. Disable it on test-repo-2 so
// `ActionsUITests.testActionsTabAbsentForRepoWithoutActions` exercises the negative path.
print("Disabling actions on test-repo-2...")
try await withRetry(maxAttempts: 5, delay: .seconds(2), operation: "disable actions") {
_ = try await repoService.editRepository(
owner: adminUser, repo: "test-repo-2", hasActions: false,
)
}
print("Updating html-readme-repo README...")
let readmeSHA: String = try await withRetry(maxAttempts: 5, delay: .seconds(2), operation: "fetch README") {
let file = try await repoService.fetchFileContent(
owner: adminUser, repo: "html-readme-repo", path: "README.md",
)
return file.sha
}
_ = try await repoService.updateFile(
owner: adminUser, repo: "html-readme-repo", path: "README.md",
content: htmlReadmeContent, sha: readmeSHA, message: "Add HTML to README",
)
}
// MARK: - Phase 3: Issues, files, metadata
// swiftlint:disable:next function_body_length
private func createIssuesFilesMetadata(
adminClient: ForgejoClient, testbotClient: ForgejoClient,
) async throws {
let adminIssueService = IssueService(client: adminClient)
let testbotIssueService = IssueService(client: testbotClient)
let repoService = RepositoryService(client: adminClient)
print("Creating pagination test issues...")
async let paginationTask: Void = {
for index in 1 ... 25 {
_ = try await adminIssueService.createIssue(
owner: adminUser, repo: "test-repo-2",
title: "Pagination test issue \(index)", body: nil,
)
}
}()
print("Creating issues as testbot...")
for index in 1 ... 3 {
_ = try await testbotIssueService.createIssue(
owner: adminUser, repo: "test-repo",
title: "Test issue \(index) from integration tests", body: nil,
)
}
print("Setting up test-repo content...")
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask {
try await withRetry(maxAttempts: 3, delay: .seconds(1), operation: "comment on issue #1") {
_ = try await testbotIssueService.createComment(
owner: adminUser, repo: "test-repo",
index: 1, body: "This is a test comment from testbot",
)
}
}
group.addTask {
try await withRetry(maxAttempts: 3, delay: .seconds(1), operation: "edit issue #3") {
_ = try await adminIssueService.editIssue(
owner: adminUser, repo: "test-repo", index: 3,
title: nil,
body: "This is the **markdown** body for issue 3.\n\nIt has multiple paragraphs.",
state: "closed",
)
}
}
group.addTask {
try await withRetry(maxAttempts: 3, delay: .seconds(1), operation: "edit issue #1") {
_ = try await adminIssueService.editIssue(
owner: adminUser, repo: "test-repo", index: 1,
title: nil,
body: "This is the **markdown** body for issue 1.\n\nIt has multiple paragraphs.",
state: nil,
)
}
}
group.addTask {
try await withRetry(maxAttempts: 3, delay: .seconds(1), operation: "edit issue #2") {
_ = try await adminIssueService.editIssue(
owner: adminUser, repo: "test-repo", index: 2,
title: nil,
body: "This is the **markdown** body for issue 2.\n\nIt has multiple paragraphs.",
state: nil,
)
}
}
group.addTask {
try await withRetry(maxAttempts: 3, delay: .seconds(1), operation: "add testbot collaborator") {
try await repoService.addCollaborator(
owner: adminUser, repo: "test-repo",
username: testbotUser, permission: "write",
)
}
}
group.addTask {
try await withRetry(maxAttempts: 3, delay: .seconds(1), operation: "add readonly collaborator") {
try await repoService.addCollaborator(
owner: adminUser, repo: "test-repo",
username: readonlyUser, permission: "read",
)
}
}
group.addTask {
try await withRetry(maxAttempts: 3, delay: .seconds(1), operation: "create label") {
_ = try await repoService.createLabel(
owner: adminUser, repo: "test-repo",
name: "bug", color: "#ee0701",
)
}
}
group.addTask {
try await withRetry(maxAttempts: 3, delay: .seconds(1), operation: "create milestone") {
_ = try await repoService.createMilestone(
owner: adminUser, repo: "test-repo",
title: "v1.0", description: "First release milestone",
dueOn: "2026-03-01T00:00:00Z",
)
}
}
try await group.waitForAll()
}
_ = try await repoService.createFile(
owner: adminUser, repo: "test-repo", path: "hello.py",
content: "print(\"Hello from Forgejo!\")",
message: "Add hello.py",
)
_ = try await repoService.createFile(
owner: adminUser, repo: "test-repo", path: "src/main.py",
content: "def main():\n print(\"Main module\")",
message: "Add src/main.py",
)
try await paginationTask
print("Phase 3 done.")
print("Setting issue metadata...")
try await withRetry(maxAttempts: 3, delay: .seconds(1), operation: "add label to issue #1") {
_ = try await adminIssueService.replaceLabels(
owner: adminUser, repo: "test-repo",
index: 1, labelIDs: [1],
)
}
try await withRetry(maxAttempts: 3, delay: .seconds(1), operation: "set milestone on issue #1") {
_ = try await adminIssueService.editIssue(
owner: adminUser, repo: "test-repo", index: 1,
title: nil, body: nil, state: nil,
milestone: 1, assignees: [adminUser],
)
}
}
// MARK: - Phase 4: PRs
private func createPullRequests(testbotClient: ForgejoClient) async throws {
let repoService = RepositoryService(client: testbotClient)
let prService = PullRequestService(client: testbotClient)
print("Creating feature branch...")
_ = try await repoService.createFile(
owner: adminUser, repo: "test-repo", path: "feature.txt",
content: "This file was added in a feature branch",
message: "Add feature.txt",
newBranch: "feature-branch",
)
print("Creating pull request #4...")
_ = try await prService.createPullRequest(
owner: adminUser, repo: "test-repo",
title: "Add feature file", head: "feature-branch", base: "main",
body: "This PR adds a new feature file.",
)
print("Creating merge branch...")
_ = try await repoService.createFile(
owner: adminUser, repo: "test-repo", path: "merge-test.txt",
content: "This file is for merge testing",
message: "Add merge-test.txt",
newBranch: "merge-branch",
)
print("Creating pull request #5...")
_ = try await prService.createPullRequest(
owner: adminUser, repo: "test-repo",
title: "Merge test PR", head: "merge-branch", base: "main",
body: "This PR is for testing the merge flow.",
)
print("Creating squash-merge branch...")
_ = try await repoService.createFile(
owner: adminUser, repo: "test-repo", path: "squash-test.txt",
content: "This file is for squash merge testing",
message: "Add squash-test.txt",
newBranch: "squash-branch",
)
print("Creating pull request #6...")
_ = try await prService.createPullRequest(
owner: adminUser, repo: "test-repo",
title: "Squash merge test PR", head: "squash-branch", base: "main",
body: "This PR is for testing the squash merge flow.",
)
print("Creating rebase branch...")
_ = try await repoService.createFile(
owner: adminUser, repo: "test-repo", path: "rebase-test.txt",
content: "This file is for rebase testing",
message: "Add rebase-test.txt",
newBranch: "rebase-branch",
)
print("Creating pull request #7...")
_ = try await prService.createPullRequest(
owner: adminUser, repo: "test-repo",
title: "Rebase merge test PR", head: "rebase-branch", base: "main",
body: "This PR is for testing the rebase merge flow.",
)
}
// MARK: - Phase 5: PR metadata
private func setPRMetadata(
adminClient: ForgejoClient, testbotClient: ForgejoClient,
) async throws {
let testbotIssueService = IssueService(client: testbotClient)
let adminPRService = PullRequestService(client: adminClient)
let adminIssueService = IssueService(client: adminClient)
print("Setting PR metadata...")
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask {
try await withRetry(maxAttempts: 3, delay: .seconds(1), operation: "comment on PR #4") {
_ = try await testbotIssueService.createComment(
owner: adminUser, repo: "test-repo",
index: 4, body: "Please review this PR when you get a chance.",
)
}
}
group.addTask {
try await withRetry(maxAttempts: 3, delay: .seconds(1), operation: "review PR #4") {
_ = try await adminPRService.createReview(
owner: adminUser, repo: "test-repo", index: 4,
body: "Looks good so far, just a few comments.",
event: "COMMENT",
comments: [
CreateReviewComment(
body: "Consider a more descriptive filename.",
path: "feature.txt",
oldPosition: nil,
newPosition: 1,
),
],
)
}
}
group.addTask {
try await withRetry(maxAttempts: 3, delay: .seconds(1), operation: "set PR #4 metadata") {
_ = try await adminIssueService.editIssue(
owner: adminUser, repo: "test-repo", index: 4,
title: nil, body: nil, state: nil,
milestone: 1, assignees: [adminUser],
)
}
}
try await group.waitForAll()
}
}
// MARK: - Phase 6: API token
private func createAPIToken(adminClient: ForgejoClient) async throws {
let userService = UserService(client: adminClient)
print("Creating API token for testadmin...")
let token = try await userService.createToken(
username: adminUser,
name: "integration-test-token",
scopes: [
"read:user", "write:user",
"read:repository", "write:repository",
"read:issue", "write:issue",
"read:notification", "write:notification",
"read:organization",
],
)
let tokenPath = "/tmp/forgejo_test_token.txt"
try token.sha1.write(toFile: tokenPath, atomically: true, encoding: .utf8)
print("API token written to \(tokenPath)")
}
}
// MARK: - HTML README content
private let htmlReadmeContent = """
# HTML Readme Test
Regular markdown paragraph.
Click to expand
This content is hidden by default.
- Item A
- Item B
| Name | Value |
|---|---|
| Alpha | 1 |
| Beta | 2 |