Skip to content

Commit f3bac9c

Browse files
authored
chore: kickoff release
2 parents 151fac5 + bd5e6bf commit f3bac9c

File tree

19 files changed

+213790
-6
lines changed

19 files changed

+213790
-6
lines changed

.github/CODEOWNERS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,7 @@
66

77
# Changes to Xcode / OS runtime versions run in CI/CD requires admin approval.
88
/.github/composite_actions/get_platform_parameters/action.yml @aws-amplify/amplify-ios-admins
9+
10+
# Changes to files in the api-dump or api-dump-test folder require admin approval.
11+
api-dump/* @aws-amplify/amplify-ios-admins
12+
api-dump-test/* @aws-amplify/amplify-ios-admins
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
name: Public Interface Breakage Detection
2+
3+
on:
4+
pull_request:
5+
6+
permissions:
7+
contents: write
8+
pull-requests: write
9+
10+
jobs:
11+
build-and-check-api-breakage:
12+
name: Build and Check API Breakage
13+
runs-on: macos-latest
14+
15+
steps:
16+
- name: Checkout repository
17+
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1
18+
with:
19+
ref: ${{ github.head_ref }} # Checkout the PR branch
20+
fetch-depth: 1
21+
22+
- name: Fetch the branchs
23+
run: |
24+
git fetch origin ${{ github.sha }}
25+
26+
27+
- name: Setup and Run Swift API Diff
28+
env:
29+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30+
run: |
31+
# Define the list of exceptions to filter out
32+
exceptions=(
33+
'has been added as a new enum case$'
34+
'is now with @_spi$'
35+
)
36+
37+
# Define the mandatory patterns to filter out
38+
mandatory_patterns=(
39+
'^/\*'
40+
'^$'
41+
)
42+
43+
# Function to apply patterns with grep
44+
apply_patterns() {
45+
local input="$1"
46+
local output="$input"
47+
48+
# Apply mandatory patterns
49+
for pattern in "${mandatory_patterns[@]}"; do
50+
output=$(echo "$output" | grep -v "$pattern")
51+
done
52+
53+
# Apply exceptions
54+
for exception in "${exceptions[@]}"; do
55+
output=$(echo "$output" | grep -v "$exception")
56+
done
57+
58+
echo "$output"
59+
}
60+
61+
echo "Swift version: $(swift --version)"
62+
echo "Swift package manager version: $(swift package --version)"
63+
swift package resolve
64+
65+
# Ensure we are in the correct directory
66+
cd $GITHUB_WORKSPACE
67+
68+
# Run swift-api-diff commands here directly
69+
NEW_API_DIR=$(mktemp -d)
70+
OLD_API_DIR=$(mktemp -d)
71+
SDK_PATH=$(xcrun --show-sdk-path)
72+
73+
# Get all library module names
74+
# Moduels with aws-crt-swift as dependency are not listed due to swift-api-digester's issue with analyzing C dependencies
75+
modules=$(swift package dump-package | jq -r '.products | map(select(.name == "Amplify" or .name == "CoreMLPredictionsPlugin" or .name == "AWSDataStorePlugin" or .name == "AWSPluginsCore")) | map(.name) | .[]')
76+
echo "Modules: $modules"
77+
78+
echo "Fetching old version..."
79+
git fetch origin ${{ github.event.pull_request.base.sha }}
80+
git checkout ${{ github.event.pull_request.base.sha }}
81+
built=false
82+
for module in $modules; do
83+
# If file doesn't exits in the old directory
84+
if [ ! -f api-dump/${module}.json ]; then
85+
echo "Old API file does not exist in the base branch. Generating it..."
86+
# Check if the project has been built
87+
if ! $built; then
88+
echo "Building project..."
89+
swift build > /dev/null 2>&1 || { echo "Failed to build project"; exit 1; }
90+
built=true
91+
fi
92+
93+
# Generate the API file using api-digester
94+
swift api-digester -sdk "$SDK_PATH" -dump-sdk -module "$module" -o "$OLD_API_DIR/${module}.json" -I .build/debug || { echo "Failed to dump new SDK for module $module"; exit 1; }
95+
else
96+
# Use the api-dump/${module}.json file from the base branch directly
97+
cp "api-dump/${module}.json" "$OLD_API_DIR/${module}.json"
98+
fi
99+
done
100+
101+
echo "Fetching new version..."
102+
git checkout ${{ github.sha }}
103+
git log -1 # Print the commit info for debugging
104+
swift build> /dev/null 2>&1 || { echo "Failed to build new version"; exit 1; }
105+
for module in $modules; do
106+
swift api-digester -sdk "$SDK_PATH" -dump-sdk -module "$module" -o "$NEW_API_DIR/${module}.json" -I .build/debug || { echo "Failed to dump new SDK for module $module"; exit 1; }
107+
done
108+
109+
# Compare APIs for each module and capture the output
110+
api_diff_output=""
111+
for module in $modules; do
112+
swift api-digester -sdk "$SDK_PATH" -diagnose-sdk --input-paths "$OLD_API_DIR/${module}.json" --input-paths "$NEW_API_DIR/${module}.json" >> "api-diff-report-${module}.txt" 2>&1
113+
module_diff_output=$(apply_patterns "$(cat "api-diff-report-${module}.txt")")
114+
if [ -n "$module_diff_output" ]; then
115+
api_diff_output="${api_diff_output}\n**Module: ${module}**\n${module_diff_output}\n"
116+
117+
# Check if there are lines containing "has been renamed to Func"
118+
if echo "$module_diff_output" | grep -q 'has been renamed to Func'; then
119+
# Capture the line containing "has been renamed to Func"
120+
renamed_line=$(echo "$module_diff_output" | grep 'has been renamed to Func')
121+
122+
# Append a message to the module_diff_output
123+
api_diff_output="${api_diff_output}👉🏻 _Note: If you're just adding optional parameters to existing methods, neglect the line:_\n_${renamed_line}_\n"
124+
fi
125+
fi
126+
done
127+
128+
echo "API_DIFF_OUTPUT<<EOF" >> $GITHUB_ENV
129+
if [ -n "$api_diff_output" ]; then
130+
echo "### 💔 Public API Breaking Change detected:" >> $GITHUB_ENV
131+
echo -e "$api_diff_output" >> $GITHUB_ENV
132+
echo "EOF" >> $GITHUB_ENV
133+
else
134+
echo "### ✅ No Public API Breaking Change detected" >> $GITHUB_ENV
135+
echo "EOF" >> $GITHUB_ENV
136+
fi
137+
138+
# Checkout to the branch associated with the pull request
139+
git stash --include-untracked
140+
git checkout ${{ github.head_ref }}
141+
142+
if [ ! -d "api-dump" ]; then
143+
echo "api-dump folder does not exist. Creating it..."
144+
mkdir -p "api-dump"
145+
fi
146+
147+
# Update the api-dump folder of the new version by making a commit if there are changes
148+
for module in $modules; do
149+
if [ ! -f api-dump/${module}.json ]; then
150+
echo "API file does not exist in api-dump folder. Creating it..."
151+
echo "{}" > "api-dump/${module}.json"
152+
fi
153+
if ! diff "$NEW_API_DIR/${module}.json" "api-dump/${module}.json" > /dev/null; then
154+
echo "Updating API Dumps..."
155+
mv "$NEW_API_DIR/${module}.json" "api-dump/${module}.json"
156+
fi
157+
done
158+
159+
git config --global user.name "aws-amplify-ops"
160+
git config --global user.email "aws-amplify@amazon.com"
161+
162+
git add api-dump/*.json
163+
164+
if ! git diff --cached --quiet --exit-code; then
165+
# Get the file names that have changes
166+
changed_files=$(git diff --cached --name-only)
167+
168+
push_changes=false
169+
for file in $changed_files; do
170+
if [[ $file == api-dump/* ]]; then
171+
# Get the number of lines in the file
172+
total_lines=$(wc -l < "$file")
173+
# Get the line numbers of the changes
174+
changed_lines=$(git diff --cached -U0 "$file" | grep -o '@@ [^ ]* [^ ]* @@' | awk '{print $3}' | cut -d ',' -f1 | sed 's/[^0-9]//g')
175+
echo "Changed lines in $file: $changed_lines"
176+
# Check if any change is not within the last 10 lines
177+
for line in $changed_lines; do
178+
if [ "$line" -le "$((total_lines - 10))" ]; then
179+
push_changes=true
180+
break
181+
fi
182+
done
183+
184+
# If any file should be pushed, break out of the loop
185+
if [ "$push_changes" = true ]; then
186+
break
187+
fi
188+
fi
189+
done
190+
191+
if [ "$push_changes" = true ]; then
192+
git commit -m "Update API dumps for new version"
193+
git push origin HEAD:${{ github.head_ref }}
194+
else
195+
echo "No changes to commit in the api-dump folder."
196+
fi
197+
else
198+
echo "No changes to commit in the api-dump folder."
199+
fi
200+
201+
git stash pop || true
202+
203+
- name: Comment on PR with API Diff
204+
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1
205+
with:
206+
github-token: ${{ secrets.GITHUB_TOKEN }}
207+
script: |
208+
const apiDiffOutput = process.env.API_DIFF_OUTPUT;
209+
const issueNumber = context.payload.pull_request.number;
210+
const owner = context.repo.owner;
211+
const repo = context.repo.repo;
212+
213+
if (apiDiffOutput && apiDiffOutput.trim().length > 0) {
214+
github.rest.issues.createComment({
215+
owner: owner,
216+
repo: repo,
217+
issue_number: issueNumber,
218+
body: `## API Breakage Report\n${apiDiffOutput}\n`
219+
});
220+
} else {
221+
console.log("No API diff output found.");
222+
}
223+
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: Swift API Digester Functionality Check
2+
3+
on:
4+
schedule:
5+
- cron: '0 15 * * *' # This will run the action every day at 3:00 pm UTC (8:00 am PDT)
6+
workflow_dispatch: # Allows manual triggering
7+
8+
jobs:
9+
check-swift-api-digester:
10+
runs-on: macos-latest
11+
12+
steps:
13+
- name: Checkout repository
14+
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1
15+
16+
- name: Check API Digester
17+
shell: bash
18+
env:
19+
WEBHOOK_URL: ${{ secrets.SLACK_API_CHECKER_WEBHOOK_URL }}
20+
run: |
21+
TEMP_DIR=$(mktemp -d)
22+
echo "Temporary directory created at $TEMP_DIR"
23+
SDK_PATH=$(xcrun --sdk macosx --show-sdk-path)
24+
echo "SDK Path: $SDK_PATH"
25+
26+
# Run swift-api-digester
27+
swift api-digester -sdk "$SDK_PATH" -diagnose-sdk --input-paths api-dump-test/A.json --input-paths api-dump-test/B.json >> "$TEMP_DIR/api-digester-output.txt" 2>&1
28+
29+
# Display the output
30+
cat "$TEMP_DIR/api-digester-output.txt"
31+
32+
if diff "$TEMP_DIR/api-digester-output.txt" api-dump-test/expected-result.txt; then
33+
echo "The output matches the expected result."
34+
else
35+
echo "The output does not match the expected result."
36+
WORKFLOW_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
37+
echo "$WORKFLOW_URL" | xargs -I {} curl -s POST "$WEBHOOK_URL" -H "Content-Type:application/json" --data '{"WORKFLOW_URL":"{}"}'
38+
exit 1
39+
fi

Amplify/Categories/Storage/Operation/Request/StorageListRequest.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ public extension StorageListRequest {
6060
@available(*, deprecated, message: "Use `path` in Storage API instead of `Options`")
6161
public let path: String?
6262

63+
/// The strategy to use when listing contents from subpaths. Defaults to [`.include`](x-source-tag://SubpathStrategy.include)
64+
///
65+
/// - Tag: StorageListRequestOptions.subpathStrategy
66+
public let subpathStrategy: SubpathStrategy
67+
6368
/// Number between 1 and 1,000 that indicates the limit of how many entries to fetch when
6469
/// retreiving file lists from the server.
6570
///
@@ -94,15 +99,47 @@ public extension StorageListRequest {
9499
public init(accessLevel: StorageAccessLevel = .guest,
95100
targetIdentityId: String? = nil,
96101
path: String? = nil,
102+
subpathStrategy: SubpathStrategy = .include,
97103
pageSize: UInt = 1000,
98104
nextToken: String? = nil,
99105
pluginOptions: Any? = nil) {
100106
self.accessLevel = accessLevel
101107
self.targetIdentityId = targetIdentityId
102108
self.path = path
109+
self.subpathStrategy = subpathStrategy
103110
self.pageSize = pageSize
104111
self.nextToken = nextToken
105112
self.pluginOptions = pluginOptions
106113
}
107114
}
108115
}
116+
117+
public extension StorageListRequest.Options {
118+
/// Represents the strategy used when listing contents from subpaths relative to the given path.
119+
///
120+
/// - Tag: StorageListRequestOptions.SubpathStrategy
121+
enum SubpathStrategy {
122+
/// Items from nested subpaths are included in the results
123+
///
124+
/// - Tag: SubpathStrategy.include
125+
case include
126+
127+
/// Items from nested subpaths are not included in the results. Their subpaths are instead grouped under [`StorageListResult.excludedSubpaths`](StorageListResult.excludedSubpaths).
128+
///
129+
/// - Parameter delimitedBy: The delimiter used to determine subpaths. Defaults to `"/"`
130+
///
131+
/// - SeeAlso: [`StorageListResult.excludedSubpaths`](x-source-tag://StorageListResult.excludedSubpaths)
132+
///
133+
/// - Tag: SubpathStrategy.excludeWithDelimiter
134+
case exclude(delimitedBy: String = "/")
135+
136+
/// Items from nested subpaths are not included in the results. Their subpaths are instead grouped under [`StorageListResult.excludedSubpaths`](StorageListResult.excludedSubpaths).
137+
///
138+
/// - SeeAlso: [`StorageListResult.excludedSubpaths`](x-source-tag://StorageListResult.excludedSubpaths)
139+
///
140+
/// - Tag: SubpathStrategy.exclude
141+
public static var exclude: SubpathStrategy {
142+
return .exclude()
143+
}
144+
}
145+
}

Amplify/Categories/Storage/Result/StorageListResult.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,13 @@ public struct StorageListResult {
1717
/// [StorageCategoryBehavior.list](x-source-tag://StorageCategoryBehavior.list).
1818
///
1919
/// - Tag: StorageListResult.init
20-
public init(items: [Item], nextToken: String? = nil) {
20+
public init(
21+
items: [Item],
22+
excludedSubpaths: [String] = [],
23+
nextToken: String? = nil
24+
) {
2125
self.items = items
26+
self.excludedSubpaths = excludedSubpaths
2227
self.nextToken = nextToken
2328
}
2429

@@ -27,6 +32,13 @@ public struct StorageListResult {
2732
/// - Tag: StorageListResult.items
2833
public var items: [Item]
2934

35+
36+
/// Array of excluded subpaths in the Result.
37+
/// This field is only populated when [`StorageListRequest.Options.subpathStrategy`](x-source-tag://StorageListRequestOptions.subpathStragegy) is set to [`.exclude()`](x-source-tag://SubpathStrategy.exclude).
38+
///
39+
/// - Tag: StorageListResult.excludedSubpaths
40+
public var excludedSubpaths: [String]
41+
3042
/// Opaque string indicating the page offset at which to resume a listing. This value is usually copied to
3143
/// [StorageListRequestOptions.nextToken](x-source-tag://StorageListRequestOptions.nextToken).
3244
///

AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService+ListBehavior.swift

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ extension AWSS3StorageService {
3131
}
3232
let input = ListObjectsV2Input(bucket: bucket,
3333
continuationToken: options.nextToken,
34-
delimiter: nil,
34+
delimiter: options.subpathStrategy.delimiter,
3535
maxKeys: Int(options.pageSize),
3636
prefix: finalPrefix,
3737
startAfter: nil)
@@ -41,7 +41,20 @@ extension AWSS3StorageService {
4141
let items = try contents.map {
4242
try StorageListResult.Item(s3Object: $0, prefix: prefix)
4343
}
44-
return StorageListResult(items: items, nextToken: response.nextContinuationToken)
44+
45+
let commonPrefixes = response.commonPrefixes ?? []
46+
let excludedSubpaths: [String] = commonPrefixes.compactMap {
47+
guard let commonPrefix = $0.prefix else {
48+
return nil
49+
}
50+
return String(commonPrefix.dropFirst(prefix.count))
51+
}
52+
53+
return StorageListResult(
54+
items: items,
55+
excludedSubpaths: excludedSubpaths,
56+
nextToken: response.nextContinuationToken
57+
)
4558
} catch let error as StorageErrorConvertible {
4659
throw error.storageError
4760
} catch {

0 commit comments

Comments
 (0)