Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce XcodeProjToGraph and TestSupport Modules for Workspace/Project Mapping #87

Open
wants to merge 22 commits into
base: main
Choose a base branch
from

Conversation

ajkolean
Copy link

@ajkolean ajkolean commented Dec 16, 2024

Description:
This pull request adds new functionality to convert .xcworkspace and .xcodeproj files into a high-level graph model (XcodeGraph). It introduces:

  1. XcodeProjToGraph Module
    A domain-focused module responsible for mapping raw Xcode structures (projects, workspaces, schemes, targets, and dependencies) into a cohesive XcodeGraph representation. This includes:

    • Mappers:

      • WorkspaceMapper: Maps .xcworkspace contents into a Workspace domain model, identifying referenced .xcodeproj files and shared schemes.
      • ProjectMapper & PackageMapper: Extracts projects and associated packages, attributes, and configuration settings, ensuring each project is represented consistently.
      • TargetMapper & SchemeMapper: Converts PBX target definitions and scheme configurations into Target and Scheme domain entities, capturing essential build, test, and run information.
      • DependencyMapper & FileDependencyMapper: Translates XcodeProj target dependencies, platform conditions, and file references into a structured graph of interdependent components.
      • BuildPhaseMapper & BuildRuleMapper: Interprets build phases and rules, relating source files, resources, and frameworks to their respective domain-level constructs.
      • SettingsMapper & ConfigurationMatcher: Analyzes build settings and configurations, normalizing them into a predictable model that supports further graph analysis or code generation steps.
    • Extensions & Helpers:
      Additional utilities to bridge the gap between raw XcodeProj objects and the domain model. For example, AbsolutePath+Extensions helps resolve file references, and Platform+Extensions assists in correctly inferring the platform from PBXTarget attributes.

    • ProcessRunner & LipoTool:
      A utility to run external processes (like lipo) and integrate their output—such as architecture detection—into the graph model. This allows for richer metadata about binary artifacts and supported architectures.

  2. TestSupport Module
    This module centralizes mocks, helpers, and utilities that streamline testing. By providing reusable mock objects (e.g., mock project providers, file structures, and build settings), it reduces boilerplate and fosters consistency across all test suites. Key highlights:

    • Mocks Folder: A collection of ready-to-use mocks for projects, targets, schemes, environment variables, and more.
    • FileCreator & Structure Utilities: Simplifies creating and handling temporary files or directory structures during tests, ensuring clean and isolated test scenarios.
  3. XcodeProjToGraphTests
    A comprehensive test suite validating each piece of the mapping logic. The tests are thoughtfully organized by domain:

    • MapperTests:

      • Build, Dependency, Settings, Target, Workspace: Ensure that each mapper converts raw Xcode structures (phases, dependencies, settings, etc.) into domain-level models accurately.
      • Project & Package: Validate correct interpretation of project attributes, packages, and configuration lists.
      • Scheme & Graph: Confirm that schemes are parsed and integrated into the workspace and project graphs, and that the overall graph mapper works as intended.
    • ParserTests (ProjectParserTests):
      Verifies that the parsing logic for .xcodeproj and .xcworkspace directories is robust and reliable, providing a consistent foundation for the mappers.

    • ProcessTests (LipoToolTests, ProcessRunnerTests):
      Checks the integration with external tools (lipo) and process running capabilities, ensuring errors are handled gracefully and output is captured correctly.

Next Steps - Integration Tests:
While this PR focuses on unit-level validation, an upcoming PR will introduce integration tests that run end-to-end scenarios using real .xcworkspace and .xcodeproj fixtures. This will confirm that the entire pipeline—from reading disk-based Xcode structures to producing a final XcodeGraph—functions correctly in practice.

While this PR focuses on core functionality, the integration tests validating end-to-end scenarios (e.g., parsing .xcworkspace and .xcodeproj into XcodeGraph) are complete. However, due to the size of the test fixtures (15,000 lines), they were removed to reduce noise.

To review and run these tests, check out this commit (before they were removed) : 75bf656. This includes real-world project fixtures and ensures the entire pipeline functions as expected in practical scenarios.

The results for these are browsable here: https://github.com/tuist/XcodeGraph/blob/75bf656adcec3b5fe7f4c034f797961361f9f584/Tests/XcodeProjToGraphTests/IntegrationTests/Projects/IntegrationTests%2BcommandLineToolWithDynamicFramework.swift

Testing & Verification:

  • Locally:
    • Run swift build to compile all modules.
    • Run swift test to execute the full test suite. All tests should pass, confirming correct functionality.
  • CI Integration:
    Once integrated with CI (e.g., GitHub Actions), these tests will run automatically on each PR or commit, ensuring ongoing stability and reliability.

No Breaking Changes:
This PR adds new modules and tests without altering existing functionality. It forms the foundation for further enhancements and refinements.

Documentation & Maintenance:
Code comments and some documentation are included to guide future developers. With a clear architecture, it will be easier to extend capabilities, add integration tests, and enhance performance or features over time.


Summary:
This PR significantly expands the repository’s capabilities by introducing XcodeProjToGraph and TestSupport modules, along with a robust test suite in XcodeProjToGraphTests. Together, they provide a structured path from raw Xcode project/workspace data to a domain-level XcodeGraph model. The next step will be to add integration tests to validate complex real-world scenarios, ensuring the tool’s practicality and reliability for future use cases.

@ajkolean ajkolean changed the title Introduce XcodeProjToGraph and TestSupport Modules, Plus Extensive Unit Tests for Workspace/Project Mapping Introduce XcodeProjToGraph and TestSupport Modules for Workspace/Project Mapping Dec 16, 2024
Copy link
Member

@fortmarek fortmarek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First of all, thanks a lot @ajkolean 👏

This work is huge and will enable powerful workflows that the Swift community has not seen, yet.

I did a first pass, trying to mostly focus on the higher level structure of the codebase.

Some of the comments are applicable across the PR, so it'd be amazing if you could have that in mind as I didn't want to repeat myself too much. I tried to point out when I saw some pattern we should imho redo in other places as well, but I might not have done this perfectly.

Overall, I am very onboard with the structure of breaking down the mapping of XcodeProj into multiple layers of individual mappers that have a simple responsibility.

Some of the patterns I'd consider changing (and these are also captured in the individual review comments):

  • Prefer structs over classes
  • Minimize the public interface of the XcodeProjToGraph module to the bare minimum. This will help us keep this repository stable.
  • Don't overdo it with running things in parallel. Benchmark first and make asynchronous only parts of the code that are actually resource intensive.
  • Use our utility for running shell commands instead of having a new implementation: https://github.com/tuist/command
  • Utilities should usually be configured directly via methods instead of passing the configuration at the init time. Unless there's a clear win by configuration utilities through init, let's keep init reserved for dependency injection.
  • Individual mappers should not be accessing layers above. For example, a PBXBuildPhase mapper should not have a notion of PBXTarget.

Let me know if we can help you in any way to proceed. This is top on my priority list, so I'll try to provide feedback swiftly here as you go through the review.

Once higher-level concerns are resolved, I'll do another pass going deeper into the individual implementations as I skimmed through some of them during the initial pass.

This must have been a ton of work and we really appreciate it – this is open source at its best 💜

Package.swift Outdated Show resolved Hide resolved
Package.swift Outdated Show resolved Hide resolved
Sources/TestSupport/Mocks/MockBuildFilesAndPhases.swift Outdated Show resolved Hide resolved
Sources/TestSupport/Mocks/MockBuildFilesAndPhases.swift Outdated Show resolved Hide resolved
Sources/TestSupport/Mocks/MockBuildFilesAndPhases.swift Outdated Show resolved Hide resolved
Sources/XcodeProjToGraph/ProcessRunner/Executable.swift Outdated Show resolved Hide resolved
/// - Throws:
/// - `ProcessRunnerError.failedToRunProcess` if `lipo` exits with a non-zero code.
/// - Any errors related to decoding or interpreting the output as architectures.
public func parseLipoArchsResult(_ result: ProcessResult) throws -> LipoArchsResult {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered using the implementation that we have in tuist: https://github.com/tuist/tuist/blob/main/Sources/TuistCore/MetadataProviders/FrameworkMetadataProvider.swift ?

I believe we can use it instead. If so, then I'd consider creating a new module called something like ProductMetadataProvider or BinaryMetadataProvider that we can then share back with tuist instead of having the implementation duplicated. If we end up doing this, I'd consider moving the whole MetadataProviders directory into XcodeGraph: https://github.com/tuist/tuist/tree/main/Sources/TuistCore/MetadataProviders

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That’s a great callout. Thanks for pointing this out! I’ve added TODO comments to explore how best to align with this or where it should live. It might not be as straightforward as dropping it in, since the FileHandler here already uses TuistSupport (though only for the Info.plist). That said, I think this could be a great opportunity to modularize FileHandling or maybe more as a whole, especially if we can align with the broader patterns in Tuist.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FileHandler should be removed in favor of FileSystem. It should be already possible. If that's the only blocking thing to move it here, then we should be good.

@ajkolean
Copy link
Author

@fortmarek
Thanks for the detailed feedback! I’m guessing waking up to what probably looked like a 7,000-line horror show wasn’t exactly fun. 🙈 I really appreciate you diving into it so thoroughly and quickly, especially with everything else on your plate, it means a lot.

I’ve gone through and addressed almost everything: cleaned up the mappers, streamlined the test data, and tightened up public access. If there’s anything I missed or further tweaks you’d suggest, I’m all ears.

Thanks again for taking the time to go through this so thoughtfully. Your sharp eye and collaborative approach made this a great process. Looking forward to any more thoughts! 🙏

Copy link
Member

@fortmarek fortmarek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some additional feedback. Mostly nits. Some bigger pieces of feedback from this round I'd like to call out:

  • When we run into unknown objects and scenarios, I'd lean into throwing instead of swallowing these objects as nil. That will make it easier for us to spot our blinds spots. Otherwise, the conversion might be lossier than one would expect and then it's gonna be hard to figure out why.
  • I'd consider removing ProjectParser and making GraphMapper as the public interface. To do that, we'll need to make a PR in XcodeProj to accept an AbsolutePath in its init
  • Let's also update XcodeProj to hold its own path – I'd like us to move away from the ProjectProvider and WorkspaceProvider helpers and use XcodeProj directly.

public static func test(
static func test(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We actually use these in tuist/tuist – this would be a breaking change. We could have a debate on whether this is a good pattern or not, but for the purposes of this PR, I'd keep these test extension in XcodeGraph as public.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A reminder on this one.

Comment on lines 18 to 23
extension AbsolutePath {
/// Maps a path by its extension to a `TargetDependency` if applicable.
///
/// - Parameter condition: Optional platform condition.
/// - Returns: A `TargetDependency` if the extension matches known dependency types.
func mapByExtension(condition: PlatformCondition?) -> TargetDependency? {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd move this to its own mapper that follows the structure of other mappers. Something akin to:

struct PathDependencyMapper: PathDependencyMapping {
  func map(_ pathDependency: AbsolutePath) -> TargetDependency? { ... }
}

case let .developer(subPath):
return try AbsolutePath(
validating: subPath,
relativeTo: try AbsolutePath(validating: "/Applications/Xcode.app/Contents/Developer")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is assuming a given Xcode path which might be different.

You'll need to do something similar to: https://github.com/tuist/tuist/blob/main/Sources/TuistSupport/Xcode/XcodeController.swift

I'd use a more simplified version of XcodeController. We'll need to use a CommandRunner, so this method will need to be asynchronous.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd try to remove this and expose the GraphMapper instead that will take an XcodeProj as an input. To do that, we'll need to update XcodeProj model to hold on to its own path and thus will require a PR in that repo – but I think it's worth doing. That way, we might be able to get rid of the ProjectProvider and WorkspaceProvider pattern.

I imagine the usage of the library something like:

import XcodeProj
import XcodeGraphMapper

let myPath = try AbsolutePath(validating: "/xcode-path")
let xcodeProj = XcodeProj(path: myPath)
let xcodeGraph = XcodeGraphMapper().map(xcodeProj)
...

I'd also update the init of XcodeProj to accept AbsolutePath from our utility library. We'll need to internally convert it to the PathKit implementation.

Comment on lines 50 to 59
init(
graphType: GraphType,
projectProviderClosure: @escaping (AbsolutePath) throws -> ProjectProviding = { path in
let xcodeProj = try XcodeProj(pathString: path.pathString)
return ProjectProvider(xcodeProjPath: path, xcodeProj: xcodeProj)
}
) {
self.graphType = graphType
self.projectProviderClosure = projectProviderClosure
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using init, let's pass these through the map method. I'd also update the GraphMapper interface to take in the XcodeProj and figure out the type by itself:

struct GraphMapper: GraphMapping {
  func map(_ xcodeProj: XcodeProj) -> XcodeGraph { ... }
}

Comment on lines 66 to 69
guard result.isEmpty else { return result }

// Fallback if no platform detected.
let product = productType?.mapProductType() ?? .app
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we consider throwing instead of falling back to .app?

}
}

return nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we throw instead?

// Direct reference to another target in the same project.
return .target(name: remoteInfo, status: .required, condition: condition)
case let .fileReference(fileReference):
guard let projectRelativePath = fileReference.path else { return nil }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd consider throwing here.

// Reference to a target in another project.
return .project(target: remoteInfo, path: absPath, status: .required, condition: condition)
case .unknownObject:
return nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also throw here.

)
}

// MARK: - Helpers
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's mark the helpers as private

@@ -3,6 +3,7 @@ import ProjectDescription

public enum Module: String, CaseIterable {
case xcodeGraph = "XcodeGraph"
case XcodeProjMapper
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
case XcodeProjMapper
case xcodeProjMapper

Nit: I'd preserve camelCasing here.

),
mainGroup: PBXGroup,
targets: [PBXTarget] = [PBXNativeTarget.test()],
schemes _: [XCScheme] = []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks unused – is that correct?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ping on this.

Comment on lines +9 to +15
let configList = XCConfigurationList(
buildConfigurations: buildConfigurations,
defaultConfigurationName: defaultConfigurationName,
defaultConfigurationIsVisible: defaultConfigurationIsVisible
)

return configList
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
let configList = XCConfigurationList(
buildConfigurations: buildConfigurations,
defaultConfigurationName: defaultConfigurationName,
defaultConfigurationIsVisible: defaultConfigurationIsVisible
)
return configList
XCConfigurationList(
buildConfigurations: buildConfigurations,
defaultConfigurationName: defaultConfigurationName,
defaultConfigurationIsVisible: defaultConfigurationIsVisible
)

}
}

//
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this can be removed?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be removed?


// MARK: - WorkspaceFixture

/// Provides references to sample Xcode workspace fixtures used for integration testing.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's create XcodeProjMapperIntegrationTests

@Suite
struct XCWorkspaceMapperTests {
@Test("Maps workspace without any projects or schemes")
func testMap_NoProjectsOrSchemes() throws {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd consider following the // Given // When // Then comments convention that we have in tuist/tuist. It makes tests easier to parse.

}

@Test("Resolves other path in XCWorkspaceDataFileRef")
func testMap_OtherPath() throws {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func testMap_OtherPath() throws {
func testMap_otherPath() throws {

nit: I would follow this casing convention

struct TargetDependencyExtensionsTests {
let sourceDirectory = try! AbsolutePath(validating: "/tmp/TestProject")

// A dummy global target map for .project dependencies
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// A dummy global target map for .project dependencies
// A dummy target map for .project dependencies

It's not global 🙂

Comment on lines 18 to 22
struct IntegrationTests {
/// Asserts that the given graph parsed from a workspace fixture matches the provided inline snapshots.
///
/// - Parameters:
/// - fixture: A closure that returns a `WorkspaceFixture` used to generate the graph.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we asserting with snapshots? I'd instead compare the parsed XcodeGraph with the expected XcodeGraph:

#expect(gotXcodeGraph, expectedXcodeGraph)

What's the reason we went with asserting on snapshot text which is more unstable than comparing the XcodeGraph directly?

Comment on lines 107 to 123
var schemes = [Scheme]()
let sharedDataPath = Path(srcPath.pathString) + "xcshareddata/xcschemes"

if sharedDataPath.exists {
let schemePaths = try sharedDataPath.children().filter { $0.extension == "xcscheme" }

// Construct graphType for schemes
let graphType = GraphType.workspace(workspaceProvider)
let schemeMapper = XCSchemeMapper()
for schemePath in schemePaths {
let xcscheme = try XCScheme(path: schemePath)
let scheme = try schemeMapper.map(xcscheme, shared: true, graphType: graphType)
schemes.append(scheme)
}
}

return schemes
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I generally prefer not using vars if not necessary as it makes it easier to spot where the final values are coming from:

Suggested change
var schemes = [Scheme]()
let sharedDataPath = Path(srcPath.pathString) + "xcshareddata/xcschemes"
if sharedDataPath.exists {
let schemePaths = try sharedDataPath.children().filter { $0.extension == "xcscheme" }
// Construct graphType for schemes
let graphType = GraphType.workspace(workspaceProvider)
let schemeMapper = XCSchemeMapper()
for schemePath in schemePaths {
let xcscheme = try XCScheme(path: schemePath)
let scheme = try schemeMapper.map(xcscheme, shared: true, graphType: graphType)
schemes.append(scheme)
}
}
return schemes
let sharedDataPath = Path(srcPath.pathString) + "xcshareddata/xcschemes"
if sharedDataPath.exists {
let schemePaths = try sharedDataPath.children().filter { $0.extension == "xcscheme" }
try schemePaths.map { schemePath in
try schemeMapper.map(
try XCScheme(path: schemePath),
shared: true,
graphType: .workspace(workspaceProvider)
)
}
} else {
return []
}

Note I removed schemeMapper initialization as that belongs into the init of XCWorkspaceMapper as it should be dependency injected

@fortmarek
Copy link
Member

That was not intended.

We should be able to remove now the ProjectParser, ProjectProvider, and WorkspaceProvider now that the path PR in XcodeProj was merged: tuist/XcodeProj#892

@ajkolean
Copy link
Author

ajkolean commented Jan 4, 2025

Apologies for the delay. I was away due to the holidays. I will respond and update from the feedback within the next few days. I haven't forgotten about it.

@fortmarek
Copy link
Member

Apologies for the delay. I was away due to the holidays. I will respond and update from the feedback within the next few days. I haven't forgotten about it.

No worries, I figured 🙂 Hope you were able to recharge over the holidays! Looking forward to updates on this, we are still very much excited about this work landing.

Copy link
Member

@fortmarek fortmarek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this next iteration! I have another round of comments, but I feel we're very close to merging this 🙂

name: "XcodeProjMapper",
dependencies: [
"XcodeGraph",
"MetadataProviders",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"MetadataProviders",
"XcodeMetadata",

I'd keep the convention in this package to use Xcode as a prefix. I feel XcodeMetadata reads a bit better as a module name than MetadataProviders.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We plan to move away from FatalError: https://community.tuist.dev/t/deprecate-fatalerror/250

Instead, we can leverage the Error's errorDescription instead.

/// the transformed values will match the original sequence,
/// except for the values that were transformed into `nil`.
/// - throws: Rethrows any error thrown by the passed closure.
public func serialCompactMap<T: Sendable>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't make these public from this module. It would be a bit surprising to have these helpers as public in a module called MetadataProviders.

}

extension ServiceContext {
public var logger: Logger? {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would keep this internal as well, so it doesn't unexpectedly spill over to the consumers

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once this PR is merged, let's not forget about removing these files from tuist/tuist and using this new module instead.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be no longer needed?

Comment on lines +21 to +34
// func createMappedGraph(
// graphType: XcodeMapperGraphType,
// projectProviders: [AbsolutePath: MockProjectProvider]
// ) throws -> XcodeGraph.Graph {
// let mapper = XcodeGraphMapper() { path in
// guard let provider = projectProviders[path] else {
// Issue.record("Unexpected project path requested: \(path)")
// throw XcodeProjMapper.XcodeProjError.noProjectsFound
// }
// return provider
// }
//
// return try mapper.map()
// }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove this?


// NOTE: - This is temporary to reduce PR noise
/// Unzips and returns path to the Fixtures directory, if not already done.
static func getFixturesPath() throws -> String {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ping on this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file can be removed?

),
mainGroup: PBXGroup,
targets: [PBXTarget] = [PBXNativeTarget.test()],
schemes _: [XCScheme] = []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ping on this.

@ajkolean
Copy link
Author

ajkolean commented Jan 9, 2025

Thanks for this next iteration! I have another round of comments, but I feel we're very close to merging this 🙂

Thanks for another round! 😅 I didn't realize I pushed these commits yet. Of course, any feedback is always appreciated, but I was planning to address or comment on anything unresolved before asking for another look. I’ll tidy things up and give you a shout when it’s fully ready! 🙏 I think most of the moving pieces are in place now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants