Skip to content

Commit 64aaf04

Browse files
leogdionclaude
andcommitted
refactor: address PR review comments for scheduled sync workflow
This commit addresses all review comments from PR #10: Code Quality Improvements: - Remove redundant envPrefix: nil parameters across configuration keys - Make environment ConfigKey optional with programmatic defaults - Add concurrency strategy documentation to workflow files Authentication Refactoring: - Create CloudKitAuthMethod enum for type-safe auth (.pemString, .pemFile) - Refactor SyncEngine to use auth enum instead of dual optional parameters - Update all commands (Sync, Export, Clear, List, Status) to use new auth pattern Multi-Environment Workflow Support: - Create reusable composite action (.github/actions/cloudkit-sync/action.yml) - Rename cloudkit-sync.yml → cloudkit-sync-dev.yml - Add cloudkit-sync-prod.yml for production environment - Separate dev/prod with independent secrets and triggers Export Reporting Enhancements: - Add JSON artifact export with full CloudKit data - Generate Markdown summary with record counts and signing statistics - Write to GitHub Actions summary page for rich formatted output All changes verified with successful build (swift build). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 07fd830 commit 64aaf04

File tree

14 files changed

+373
-122
lines changed

14 files changed

+373
-122
lines changed
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
name: 'CloudKit Sync Action'
2+
description: 'Reusable action for syncing data to CloudKit with export reporting'
3+
4+
inputs:
5+
environment:
6+
description: 'CloudKit environment (development or production)'
7+
required: true
8+
container-id:
9+
description: 'CloudKit container ID'
10+
required: true
11+
cloudkit-key-id:
12+
description: 'CloudKit S2S key ID'
13+
required: true
14+
cloudkit-private-key:
15+
description: 'CloudKit S2S private key (PEM content)'
16+
required: true
17+
virtualbuddy-api-key:
18+
description: 'VirtualBuddy TSS API key'
19+
required: true
20+
enable-export:
21+
description: 'Run export after sync and generate reports'
22+
required: false
23+
default: 'true'
24+
25+
runs:
26+
using: "composite"
27+
steps:
28+
- name: Download latest built binary
29+
uses: dawidd6/action-download-artifact@v3
30+
with:
31+
workflow: bushel-cloud-build.yml
32+
workflow_conclusion: success
33+
name: bushel-cloud-binary
34+
path: ./binary
35+
36+
- name: Make binary executable
37+
shell: bash
38+
run: chmod +x ./binary/bushel-cloud
39+
40+
- name: Validate required secrets
41+
shell: bash
42+
env:
43+
VIRTUALBUDDY_API_KEY: ${{ inputs.virtualbuddy-api-key }}
44+
run: |
45+
if [ -z "$VIRTUALBUDDY_API_KEY" ]; then
46+
echo "❌ Error: VIRTUALBUDDY_API_KEY is not set"
47+
echo "Please add VIRTUALBUDDY_API_KEY to repository secrets"
48+
exit 1
49+
fi
50+
echo "✅ All required secrets are present"
51+
52+
- name: Run CloudKit sync
53+
shell: bash
54+
env:
55+
CLOUDKIT_KEY_ID: ${{ inputs.cloudkit-key-id }}
56+
CLOUDKIT_PRIVATE_KEY: ${{ inputs.cloudkit-private-key }}
57+
CLOUDKIT_ENVIRONMENT: ${{ inputs.environment }}
58+
CLOUDKIT_CONTAINER_ID: ${{ inputs.container-id }}
59+
VIRTUALBUDDY_API_KEY: ${{ inputs.virtualbuddy-api-key }}
60+
run: |
61+
echo "Starting CloudKit sync..."
62+
echo "Container: $CLOUDKIT_CONTAINER_ID"
63+
echo "Environment: $CLOUDKIT_ENVIRONMENT"
64+
65+
./binary/bushel-cloud sync \
66+
--verbose \
67+
--container-identifier "$CLOUDKIT_CONTAINER_ID"
68+
69+
- name: Run export and generate reports
70+
if: inputs.enable-export == 'true'
71+
shell: bash
72+
env:
73+
CLOUDKIT_KEY_ID: ${{ inputs.cloudkit-key-id }}
74+
CLOUDKIT_PRIVATE_KEY: ${{ inputs.cloudkit-private-key }}
75+
CLOUDKIT_ENVIRONMENT: ${{ inputs.environment }}
76+
CLOUDKIT_CONTAINER_ID: ${{ inputs.container-id }}
77+
run: |
78+
echo "Exporting CloudKit data..."
79+
80+
# Export to JSON
81+
./binary/bushel-cloud export \
82+
--output "cloudkit-export-${{ inputs.environment }}.json" \
83+
--pretty \
84+
--verbose \
85+
--container-identifier "$CLOUDKIT_CONTAINER_ID"
86+
87+
# Parse JSON to extract counts
88+
RESTORE_COUNT=$(jq '.restoreImages | length' "cloudkit-export-${{ inputs.environment }}.json")
89+
XCODE_COUNT=$(jq '.xcodeVersions | length' "cloudkit-export-${{ inputs.environment }}.json")
90+
SWIFT_COUNT=$(jq '.swiftVersions | length' "cloudkit-export-${{ inputs.environment }}.json")
91+
TOTAL_COUNT=$((RESTORE_COUNT + XCODE_COUNT + SWIFT_COUNT))
92+
93+
# Count signed restore images
94+
SIGNED_COUNT=$(jq '[.restoreImages[] | select(.fields.isSigned == "int64(1)")] | length' "cloudkit-export-${{ inputs.environment }}.json" || echo "0")
95+
96+
# Generate markdown summary
97+
cat > export-summary.md <<EOF
98+
# CloudKit Export Summary
99+
100+
**Environment**: \`${{ inputs.environment }}\`
101+
**Container**: \`${{ inputs.container-id }}\`
102+
**Export Date**: $(date -u +"%Y-%m-%d %H:%M:%S UTC")
103+
104+
## Record Counts
105+
106+
| Record Type | Count |
107+
|-------------|-------|
108+
| Restore Images | ${RESTORE_COUNT} |
109+
| Xcode Versions | ${XCODE_COUNT} |
110+
| Swift Versions | ${SWIFT_COUNT} |
111+
| **Total** | **${TOTAL_COUNT}** |
112+
113+
## Restore Image Status
114+
115+
- **Signed**: ${SIGNED_COUNT} images currently signed by Apple
116+
- **Unsigned**: $((RESTORE_COUNT - SIGNED_COUNT)) images no longer signed
117+
118+
## Artifacts
119+
120+
- 📄 Full export data: \`cloudkit-export-${{ inputs.environment }}.json\`
121+
- 📊 This summary: \`export-summary.md\`
122+
EOF
123+
124+
# Append to GitHub Actions summary
125+
cat export-summary.md >> $GITHUB_STEP_SUMMARY
126+
127+
echo "✅ Export complete with ${TOTAL_COUNT} total records"
128+
129+
- name: Upload export artifacts
130+
if: inputs.enable-export == 'true'
131+
uses: actions/upload-artifact@v4
132+
with:
133+
name: cloudkit-export-${{ inputs.environment }}
134+
path: |
135+
cloudkit-export-${{ inputs.environment }}.json
136+
export-summary.md
137+
retention-days: 30

.github/workflows/bushel-cloud-build.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ on:
1414
pull_request:
1515

1616
# Prevent concurrent builds
17+
# Why cancel-in-progress?
18+
# - Newer code changes supersede older builds
19+
# - Saves CI minutes by canceling outdated builds
20+
# - Each branch gets independent builds via ${{ github.ref }}
1721
concurrency:
1822
group: bushel-cloud-build-${{ github.ref }}
1923
cancel-in-progress: true
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: Scheduled CloudKit Sync (Development)
2+
3+
on:
4+
# Run three times daily (every ~8 hours) with randomized minutes
5+
# Staggered to avoid predictable patterns and align with VirtualBuddy TSS cache (12h)
6+
schedule:
7+
- cron: '17 2 * * *' # 02:17 UTC
8+
- cron: '43 10 * * *' # 10:43 UTC
9+
- cron: '29 18 * * *' # 18:29 UTC
10+
11+
# Allow manual trigger for testing
12+
workflow_dispatch:
13+
14+
# Run after successful build (for testing on feature branch)
15+
workflow_run:
16+
workflows: ["Build bushel-cloud Binary"]
17+
types: [completed]
18+
branches:
19+
- 8-scheduled-job
20+
21+
# Prevent concurrent sync runs
22+
# Why cancel-in-progress?
23+
# - Only latest sync matters (syncs are idempotent)
24+
# - Prevents race conditions when writing to CloudKit
25+
# - Saves resources by canceling redundant syncs
26+
concurrency:
27+
group: cloudkit-sync-dev
28+
cancel-in-progress: true
29+
30+
jobs:
31+
sync-dev:
32+
name: Sync to CloudKit (Development)
33+
runs-on: ubuntu-latest
34+
timeout-minutes: 30
35+
# Only run if triggered by schedule/manual, OR if build workflow succeeded
36+
if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success'
37+
38+
permissions:
39+
contents: read # Read repository code
40+
41+
steps:
42+
- name: Checkout repository
43+
uses: actions/checkout@v4
44+
45+
- name: CloudKit Sync
46+
uses: ./.github/actions/cloudkit-sync
47+
with:
48+
environment: development
49+
container-id: iCloud.com.brightdigit.Bushel
50+
cloudkit-key-id: ${{ secrets.CLOUDKIT_KEY_ID }}
51+
cloudkit-private-key: ${{ secrets.CLOUDKIT_PRIVATE_KEY }}
52+
virtualbuddy-api-key: ${{ secrets.VIRTUALBUDDY_API_KEY }}
53+
enable-export: 'true'
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Scheduled CloudKit Sync (Production)
2+
3+
on:
4+
# Manual trigger only for production
5+
# Recommend running after testing in development
6+
workflow_dispatch:
7+
8+
# Optional: Less frequent scheduled sync for production
9+
# Uncomment to enable once production is ready
10+
# schedule:
11+
# - cron: '0 6 * * *' # Once daily at 6:00 UTC
12+
13+
# Prevent concurrent sync runs
14+
# Why cancel-in-progress?
15+
# - Only latest sync matters (syncs are idempotent)
16+
# - Prevents race conditions when writing to CloudKit
17+
# - Saves resources by canceling redundant syncs
18+
concurrency:
19+
group: cloudkit-sync-prod
20+
cancel-in-progress: true
21+
22+
jobs:
23+
sync-prod:
24+
name: Sync to CloudKit (Production)
25+
runs-on: ubuntu-latest
26+
timeout-minutes: 30
27+
28+
permissions:
29+
contents: read # Read repository code
30+
31+
steps:
32+
- name: Checkout repository
33+
uses: actions/checkout@v4
34+
35+
- name: CloudKit Sync
36+
uses: ./.github/actions/cloudkit-sync
37+
with:
38+
environment: production
39+
container-id: iCloud.com.brightdigit.Bushel
40+
cloudkit-key-id: ${{ secrets.CLOUDKIT_KEY_ID_PROD }}
41+
cloudkit-private-key: ${{ secrets.CLOUDKIT_PRIVATE_KEY_PROD }}
42+
virtualbuddy-api-key: ${{ secrets.VIRTUALBUDDY_API_KEY }}
43+
enable-export: 'true'

.github/workflows/cloudkit-sync.yml

Lines changed: 0 additions & 77 deletions
This file was deleted.

Sources/BushelCloudCLI/Commands/ClearCommand.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,20 @@ enum ClearCommand {
5959
}
6060
}
6161

62+
// Determine authentication method
63+
let authMethod: CloudKitAuthMethod
64+
if let pemString = config.cloudKit.privateKey {
65+
authMethod = .pemString(pemString)
66+
} else {
67+
authMethod = .pemFile(path: config.cloudKit.privateKeyPath)
68+
}
69+
6270
// Create sync engine
6371
let syncEngine = try SyncEngine(
6472
containerIdentifier: config.cloudKit.containerID,
6573
keyID: config.cloudKit.keyID,
66-
privateKeyPath: config.cloudKit.privateKeyPath
74+
authMethod: authMethod,
75+
environment: config.cloudKit.environment
6776
)
6877

6978
// Execute clear

Sources/BushelCloudCLI/Commands/ExportCommand.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,20 @@ enum ExportCommand {
4242
// Enable verbose console output if requested
4343
ConsoleOutput.isVerbose = config.export?.verbose ?? false
4444

45+
// Determine authentication method
46+
let authMethod: CloudKitAuthMethod
47+
if let pemString = config.cloudKit.privateKey {
48+
authMethod = .pemString(pemString)
49+
} else {
50+
authMethod = .pemFile(path: config.cloudKit.privateKeyPath)
51+
}
52+
4553
// Create sync engine
4654
let syncEngine = try SyncEngine(
4755
containerIdentifier: config.cloudKit.containerID,
4856
keyID: config.cloudKit.keyID,
49-
privateKeyPath: config.cloudKit.privateKeyPath
57+
authMethod: authMethod,
58+
environment: config.cloudKit.environment
5059
)
5160

5261
// Execute export

Sources/BushelCloudCLI/Commands/ListCommand.swift

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,22 @@ enum ListCommand {
4040
let config = try rawConfig.validated()
4141

4242
// Create CloudKit service
43-
let cloudKitService = try BushelCloudKitService(
44-
containerIdentifier: config.cloudKit.containerID,
45-
keyID: config.cloudKit.keyID,
46-
privateKeyPath: config.cloudKit.privateKeyPath
47-
)
43+
let cloudKitService: BushelCloudKitService
44+
if let pemString = config.cloudKit.privateKey {
45+
cloudKitService = try BushelCloudKitService(
46+
containerIdentifier: config.cloudKit.containerID,
47+
keyID: config.cloudKit.keyID,
48+
pemString: pemString,
49+
environment: config.cloudKit.environment
50+
)
51+
} else {
52+
cloudKitService = try BushelCloudKitService(
53+
containerIdentifier: config.cloudKit.containerID,
54+
keyID: config.cloudKit.keyID,
55+
privateKeyPath: config.cloudKit.privateKeyPath,
56+
environment: config.cloudKit.environment
57+
)
58+
}
4859

4960
// Determine what to list based on flags
5061
let listConfig = config.list

0 commit comments

Comments
 (0)