Skip to content

Commit da1e495

Browse files
authored
Merge pull request #100 from Adyen/package-analyzer-fixes
Package analyzer fixes
2 parents 73461d4 + b7dd50c commit da1e495

File tree

6 files changed

+249
-166
lines changed

6 files changed

+249
-166
lines changed

.github/workflows/detect-api-changes.yml

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,34 +31,61 @@ jobs:
3131
uses: actions/checkout@v4
3232
with:
3333
fetch-depth: 0
34-
34+
3535
- name: 👾 Define Diff Versions
3636
run: |
37-
NEW="${{ env.source }}~${{ env.githubRepo }}"
38-
if [[ '${{ github.head_ref || env.noTargetBranch }}' == release/* ]]
37+
NEW="${{ env.source }}~${{ env.headGithubRepo }}"
38+
OLD="${{ env.target }}~${{ env.baseGithubRepo }}"
39+
40+
if [[ '${{ env.targetBranchName || env.noTargetBranch }}' == release/* ]]
3941
then
4042
LATEST_TAG=$(git describe --tags --abbrev=0)
41-
OLD="$LATEST_TAG~${{ env.githubRepo }}"
42-
else
43-
OLD="${{ env.target }}~${{ env.githubRepo }}"
43+
OLD="$LATEST_TAG~${{ env.baseGithubRepo }}"
4444
fi
4545
46-
echo "OLD=$OLD"
47-
echo "NEW=$NEW"
48-
4946
# Providing the output to the environment
5047
echo "OLD_VERSION=$OLD" >> $GITHUB_ENV
5148
echo "NEW_VERSION=$NEW" >> $GITHUB_ENV
5249
env:
5350
source: '${{ github.event.inputs.new || github.head_ref }}'
5451
target: '${{ github.event.inputs.old || github.event.pull_request.base.ref }}'
55-
githubRepo: '${{github.server_url}}/${{github.repository}}.git'
52+
headGithubRepo: '${{github.server_url}}/${{ github.event.pull_request.head.repo.full_name || github.repository}}.git'
53+
baseGithubRepo: '${{github.server_url}}/${{github.repository}}.git'
5654
noTargetBranch: 'no target branch'
55+
targetBranchName: '${{ github.head_ref }}'
56+
57+
- name: 🧰 Build Swift CLI
58+
run: swift build --configuration release
59+
60+
- name: 🏃 Run Diff
61+
run: |
62+
NEW=${{ env.NEW_VERSION }}
63+
OLD=${{ env.OLD_VERSION }}
64+
PLATFORM="macOS"
65+
PROJECT_FOLDER=${{ github.workspace }}
66+
BINARY_PATH="$(swift build --configuration release --show-bin-path)/public-api-diff"
67+
68+
echo "▶️ Running binary at $BINARY_PATH"
69+
$BINARY_PATH project --new "$NEW" --old "$OLD" --platform "$PLATFORM" --output "$PROJECT_FOLDER/api_comparison.md" --log-output "$PROJECT_FOLDER/logs.txt"
70+
cat "$PROJECT_FOLDER/logs.txt"
71+
72+
if [[ ${{ env.HEAD_GITHUB_REPO != env.BASE_GITHUB_REPO }} ]]; then
73+
echo "---" >> $GITHUB_STEP_SUMMARY
74+
echo "> [!IMPORTANT]" >> $GITHUB_STEP_SUMMARY
75+
echo "> **Commenting on pull requests from forks is not possible** due to insufficient permissions." >> $GITHUB_STEP_SUMMARY
76+
echo "> Once merged, the output will be posted as an auto-updating comment under the pull request." >> $GITHUB_STEP_SUMMARY
77+
echo "---" >> $GITHUB_STEP_SUMMARY
78+
fi
79+
80+
cat "$PROJECT_FOLDER/api_comparison.md" >> $GITHUB_STEP_SUMMARY
5781
58-
- name: 🔍 Detect Changes
59-
uses: ./ # Uses the action.yml that's at the root of the repository
60-
id: public_api_diff
82+
# We only want to comment if we're in a Pull Request and if the Pull Request is not from a forked Repository
83+
# Forked Repositories have different rights for security reasons and thus it's not possible to comment on PRs without lowering the security
84+
# once the tool is merged the base repo rights apply and the script can comment on PRs as expected.
85+
- if: ${{ github.event.pull_request.base.ref != '' && env.HEAD_GITHUB_REPO == env.BASE_GITHUB_REPO }}
86+
name: 📝 Comment on PR
87+
uses: thollander/actions-comment-pull-request@v3
6188
with:
62-
platform: "macOS"
63-
new: ${{ env.NEW_VERSION }}
64-
old: ${{ env.OLD_VERSION }}
89+
file-path: "${{ github.workspace }}/api_comparison.md"
90+
comment-tag: api_changes
91+
mode: recreate
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
//
2+
// SwiftPackageFileAnalyzer+Targets.swift
3+
// public-api-diff
4+
//
5+
// Created by Alexander Guretzki on 10/02/2025.
6+
//
7+
8+
import Foundation
9+
10+
import PADCore
11+
import PADLogging
12+
13+
import FileHandlingModule
14+
import ShellModule
15+
import SwiftPackageFileHelperModule
16+
17+
extension SwiftPackageFileAnalyzer {
18+
19+
internal func analyzeTargets(
20+
old: [SwiftPackageDescription.Target],
21+
new: [SwiftPackageDescription.Target],
22+
oldProjectBasePath: String,
23+
newProjectBasePath: String
24+
) throws -> [Change] {
25+
guard old != new else { return [] }
26+
27+
let oldTargetNames = Set(old.map(\.name))
28+
let newTargetNames = Set(new.map(\.name))
29+
30+
let added = newTargetNames.subtracting(oldTargetNames)
31+
let removed = oldTargetNames.subtracting(newTargetNames)
32+
let consistent = Set(oldTargetNames).intersection(Set(newTargetNames))
33+
34+
var changes = [Change]()
35+
36+
changes += added.compactMap { addition in
37+
guard let addedTarget = new.first(where: { $0.name == addition }) else { return nil }
38+
return .init(
39+
changeType: .addition(description: addedTarget.description),
40+
parentPath: Constants.packageFileName(child: "targets")
41+
)
42+
}
43+
44+
try consistent.forEach { productName in
45+
guard
46+
let oldTarget = old.first(where: { $0.name == productName }),
47+
let newTarget = new.first(where: { $0.name == productName })
48+
else { return }
49+
50+
changes += try analyzeTarget(
51+
oldTarget: oldTarget,
52+
newTarget: newTarget,
53+
oldProjectBasePath: oldProjectBasePath,
54+
newProjectBasePath: newProjectBasePath
55+
)
56+
}
57+
58+
changes += removed.compactMap { removal in
59+
guard let removedTarget = old.first(where: { $0.name == removal }) else { return nil }
60+
return .init(
61+
changeType: .removal(description: removedTarget.description),
62+
parentPath: Constants.packageFileName(child: "targets")
63+
)
64+
}
65+
66+
return changes
67+
}
68+
69+
private func analyzeTarget(
70+
oldTarget: SwiftPackageDescription.Target,
71+
newTarget: SwiftPackageDescription.Target,
72+
oldProjectBasePath: String,
73+
newProjectBasePath: String
74+
) throws -> [Change] {
75+
guard oldTarget != newTarget else { return [] }
76+
77+
var listOfChanges = analyzeDependencies(
78+
oldTarget: oldTarget,
79+
newTarget: newTarget
80+
)
81+
82+
listOfChanges += try analyzeTargetResources(
83+
oldResources: oldTarget.resources ?? [],
84+
newResources: newTarget.resources ?? [],
85+
oldProjectBasePath: oldProjectBasePath,
86+
newProjectBasePath: newProjectBasePath
87+
)
88+
89+
if oldTarget.path != newTarget.path {
90+
listOfChanges += ["Changed path from \"\(oldTarget.path)\" to \"\(newTarget.path)\""]
91+
}
92+
93+
if oldTarget.type != newTarget.type {
94+
listOfChanges += ["Changed type from `.\(oldTarget.type.description)` to `.\(newTarget.type.description)`"]
95+
}
96+
97+
guard oldTarget.description != newTarget.description || !listOfChanges.isEmpty else { return [] }
98+
99+
return [.init(
100+
changeType: .modification(
101+
oldDescription: oldTarget.description,
102+
newDescription: newTarget.description
103+
),
104+
parentPath: Constants.packageFileName(child: "targets"),
105+
listOfChanges: listOfChanges
106+
)]
107+
108+
}
109+
}
110+
111+
// MARK: - SwiftPackageDescription.Target.Resource
112+
113+
private extension SwiftPackageFileAnalyzer {
114+
115+
func analyzeDependencies(
116+
oldTarget: SwiftPackageDescription.Target,
117+
newTarget: SwiftPackageDescription.Target
118+
) -> [String] {
119+
120+
let oldTargetDependencies = Set(oldTarget.targetDependencies ?? [])
121+
let newTargetDependencies = Set(newTarget.targetDependencies ?? [])
122+
123+
let addedTargetDependencies = newTargetDependencies.subtracting(oldTargetDependencies)
124+
let removedTargetDependencies = oldTargetDependencies.subtracting(newTargetDependencies)
125+
126+
let oldProductDependencies = Set(oldTarget.productDependencies ?? [])
127+
let newProductDependencies = Set(newTarget.productDependencies ?? [])
128+
129+
let addedProductDependencies = newProductDependencies.subtracting(oldProductDependencies)
130+
let removedProductDependencies = oldProductDependencies.subtracting(newProductDependencies)
131+
132+
var listOfChanges = [String]()
133+
listOfChanges += addedTargetDependencies.map { "Added dependency .target(name: \"\($0)\")" }
134+
listOfChanges += addedProductDependencies.map { "Added dependency .product(name: \"\($0)\", ...)" }
135+
listOfChanges += removedTargetDependencies.map { "Removed dependency .target(name: \"\($0)\")" }
136+
listOfChanges += removedProductDependencies.map { "Removed dependency .product(name: \"\($0)\", ...)" }
137+
return listOfChanges
138+
}
139+
140+
func analyzeTargetResources(
141+
oldResources old: [SwiftPackageDescription.Target.Resource],
142+
newResources new: [SwiftPackageDescription.Target.Resource],
143+
oldProjectBasePath: String,
144+
newProjectBasePath: String
145+
) throws -> [String] {
146+
147+
let oldResources = old.map { resource in
148+
var updated = resource
149+
updated.path = updated.path.trimmingPrefix(oldProjectBasePath)
150+
return updated
151+
}
152+
153+
let newResources = new.map { resource in
154+
var updated = resource
155+
updated.path = updated.path.trimmingPrefix(newProjectBasePath)
156+
return updated
157+
}
158+
159+
let oldResourcePaths = Set(oldResources.map(\.path))
160+
let newResourcePaths = Set(newResources.map(\.path))
161+
162+
let addedResourcePaths = newResourcePaths.subtracting(oldResourcePaths)
163+
let consistentResourcePaths = oldResourcePaths.intersection(newResourcePaths)
164+
let removedResourcePaths = oldResourcePaths.subtracting(newResourcePaths)
165+
166+
var listOfChanges = [String]()
167+
168+
listOfChanges += addedResourcePaths.compactMap { path in
169+
guard let resource = newResources.first(where: { $0.path.trimmingPrefix(newProjectBasePath) == path }) else { return nil }
170+
return "Added resource \(resource.description)"
171+
}
172+
173+
listOfChanges += consistentResourcePaths.compactMap { path in
174+
guard
175+
let newResource = newResources.first(where: { $0.path == path }),
176+
let oldResource = oldResources.first(where: { $0.path == path }),
177+
newResource.description != oldResource.description
178+
else { return nil }
179+
180+
return "Changed resource from `\(oldResource.description)` to `\(newResource.description)`"
181+
}
182+
183+
listOfChanges += removedResourcePaths.compactMap { path in
184+
guard let resource = oldResources.first(where: { $0.path == path }) else { return nil }
185+
return "Removed resource \(resource.description)"
186+
}
187+
188+
return listOfChanges
189+
}
190+
}
191+
192+
// MARK: - Convenience Extension
193+
194+
private extension String {
195+
func trimmingPrefix(_ prefix: String) -> String {
196+
var trimmed = self
197+
trimmed.trimPrefix(prefix)
198+
return trimmed
199+
}
200+
}

0 commit comments

Comments
 (0)