Skip to content

Commit 06e410c

Browse files
Export the Study Bundle as a zstd Archive by Default (#43)
### ♻️ Current situation & Problem The exporter's default output was an Apple Archive, which only Apple platforms can read — the Android app cannot consume the artifact the publish workflow uploads to the storage bucket. Builds on SchmiedmayerLab/Grove#44, which makes the study bundle format platform-neutral. ### ⚙️ Release Notes - The exporter defaults to a zstd-compressed tar archive (`mhcStudyBundle.spezistudybundle.tar.zst`); the Apple Archive format stays available behind `--format archive`, deprecated. The publish workflow uploads the new artifact. - The ECG active task's title and subtitle are authored as localization dictionaries, following the new study definition schema (`0.14.0`); the study revision is `43`. - `Sleep+en-UK.md` is renamed to `Sleep+en-GB.md` — `UK` is not a valid region code; Darwin silently canonicalized it while Linux does not, making exports platform-dependent. - The exporter package builds and its tests pass on Linux, so the export can run on any CI runner. - The Spezi dependency tracks the feature branch until the next Spezi release; switch to the version requirement once it is tagged. ### 📚 Documentation Inline documentation on the changed format handling. ### ✅ Testing `swift test` passes on macOS and Linux (Docker, swift:6.3). Exports are byte-identical across runs and platforms; the Android repository unpacks the produced archive in its unit tests. ### Code of Conduct & Contributing Guidelines By creating and submitting this pull request, you agree to follow our [Code of Conduct](https://github.com/SchmiedmayerLab/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/SchmiedmayerLab/.github/blob/main/CONTRIBUTING.md): - [x] I agree to follow the [Code of Conduct](https://github.com/SchmiedmayerLab/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/SchmiedmayerLab/.github/blob/main/CONTRIBUTING.md).
1 parent d148a0c commit 06e410c

11 files changed

Lines changed: 77 additions & 33 deletions

File tree

.github/workflows/publish-study-definition.yml

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ concurrency:
3737
group: ${{ inputs.environment || 'staging' }}
3838
cancel-in-progress: false
3939

40+
permissions:
41+
contents: read
42+
4043
jobs:
4144
determineenvironment:
4245
name: Determine Environment
@@ -87,7 +90,7 @@ jobs:
8790
uses: actions/upload-artifact@v4
8891
with:
8992
name: study-bundle
90-
path: mhcStudyBundle.spezistudybundle.aar
93+
path: mhcStudyBundle.spezistudybundle.tar.zst
9194

9295
authenticate-and-publish:
9396
name: (Development/Staging) Publish Study Bundle to Firebase Storage
@@ -127,9 +130,9 @@ jobs:
127130
128131
- name: Upload study bundle to Firebase Storage
129132
run: |
130-
gsutil cp mhcStudyBundle.spezistudybundle.aar gs://${{ needs.vars.outputs.FIREBASE_PROJECT_ID }}.firebasestorage.app/public/
133+
gsutil cp mhcStudyBundle.spezistudybundle.tar.zst gs://${{ needs.vars.outputs.FIREBASE_PROJECT_ID }}.firebasestorage.app/public/
131134
132-
gsutil acl ch -u AllUsers:R gs://${{ needs.vars.outputs.FIREBASE_PROJECT_ID }}.firebasestorage.app/public/mhcStudyBundle.spezistudybundle.aar
135+
gsutil acl ch -u AllUsers:R gs://${{ needs.vars.outputs.FIREBASE_PROJECT_ID }}.firebasestorage.app/public/mhcStudyBundle.spezistudybundle.tar.zst
133136
134137
- name: Verify upload
135138
run: |
@@ -184,9 +187,9 @@ jobs:
184187
185188
- name: (US production) Upload study bundle to Firebase Storage
186189
run: |
187-
gsutil cp mhcStudyBundle.spezistudybundle.aar gs://${{ needs.vars.outputs.FIREBASE_PROJECT_ID_PRODUCTION_US }}.firebasestorage.app/public/
190+
gsutil cp mhcStudyBundle.spezistudybundle.tar.zst gs://${{ needs.vars.outputs.FIREBASE_PROJECT_ID_PRODUCTION_US }}.firebasestorage.app/public/
188191
189-
gsutil acl ch -u AllUsers:R gs://${{ needs.vars.outputs.FIREBASE_PROJECT_ID_PRODUCTION_US }}.firebasestorage.app/public/mhcStudyBundle.spezistudybundle.aar
192+
gsutil acl ch -u AllUsers:R gs://${{ needs.vars.outputs.FIREBASE_PROJECT_ID_PRODUCTION_US }}.firebasestorage.app/public/mhcStudyBundle.spezistudybundle.tar.zst
190193
191194
- name: (US production) Verify upload
192195
run: |
@@ -218,9 +221,9 @@ jobs:
218221
- name: (UK production) Upload study bundle to Firebase Storage
219222
if: ${{ false }}
220223
run: |
221-
gsutil cp mhcStudyBundle.spezistudybundle.aar gs://${{ needs.vars.outputs.FIREBASE_PROJECT_ID_PRODUCTION_UK }}.firebasestorage.app/public/
224+
gsutil cp mhcStudyBundle.spezistudybundle.tar.zst gs://${{ needs.vars.outputs.FIREBASE_PROJECT_ID_PRODUCTION_UK }}.firebasestorage.app/public/
222225
223-
gsutil acl ch -u AllUsers:R gs://${{ needs.vars.outputs.FIREBASE_PROJECT_ID_PRODUCTION_UK }}.firebasestorage.app/public/mhcStudyBundle.spezistudybundle.aar
226+
gsutil acl ch -u AllUsers:R gs://${{ needs.vars.outputs.FIREBASE_PROJECT_ID_PRODUCTION_UK }}.firebasestorage.app/public/mhcStudyBundle.spezistudybundle.tar.zst
224227
225228
- name: (UK production) Verify upload
226229
if: ${{ false }}

.github/workflows/static-analysis.yml

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,8 @@ concurrency:
1919
group: Static-Analysis-${{ github.ref }}
2020
cancel-in-progress: true
2121

22+
# REUSE and Markdown Links run as part of the Repository Standards workflow.
2223
jobs:
23-
reuse_action:
24-
name: REUSE Compliance Check
25-
uses: SchmiedmayerLab/.github/.github/workflows/reuse.yml@v0.5
2624
swiftlint:
2725
name: SwiftLint
2826
uses: SchmiedmayerLab/.github/.github/workflows/swiftlint.yml@v0.5
29-
markdown_link_check:
30-
name: Markdown Link Check
31-
permissions:
32-
contents: read
33-
pull-requests: read
34-
uses: SchmiedmayerLab/.github/.github/workflows/markdown-links.yml@v0.5

.github/workflows/verify-study-bundle.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,10 @@ jobs:
2626
with:
2727
runsonlabels: '["macOS", "self-hosted"]'
2828
customcommand: 'swift test'
29+
verify_study_bundle_linux:
30+
name: Verify Study Bundle Integrity (Linux)
31+
uses: SchmiedmayerLab/.github/.github/workflows/swift-test.yml@v0.5
32+
permissions:
33+
contents: read
34+
with:
35+
runs_on_labels: '["ubuntu-latest"]'

Package.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,15 @@ let package = Package(
2626
.executable(name: "MHCStudyDefinitionExporterCLI", targets: ["MHCStudyDefinitionExporterCLI"])
2727
],
2828
dependencies: [
29-
.package(url: "https://github.com/SchmiedmayerLab/Spezi.git", .upToNextMinor(from: "0.2.0")),
29+
.package(url: "https://github.com/SchmiedmayerLab/Spezi.git", .upToNextMinor(from: "0.2.1")),
3030
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.6.2")
3131
],
3232
targets: [
3333
.target(
3434
name: "MHCStudyDefinition",
3535
dependencies: [
36-
.product(name: "SpeziStudyDefinition", package: "Spezi")
36+
.product(name: "SpeziStudyDefinition", package: "Spezi"),
37+
.product(name: "SpeziLocalization", package: "Spezi")
3738
],
3839
resources: [.process("Resources")]
3940
),

Sources/MHCStudyDefinition/Shared.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
// SPDX-License-Identifier: MIT
77
//
88

9+
import SpeziLocalization
910
import SpeziStudyDefinition
1011

1112

@@ -18,7 +19,7 @@ extension StudyDefinition.CustomActiveTaskComponent.ActiveTask {
1819
/// The My Heart Counts app's ECG active task.
1920
public static let ecg = Self(
2021
identifier: "edu.stanford.MyHeartCounts.activeTask.ecg",
21-
title: "ECG",
22-
subtitle: "Record an ECG using your Apple Watch"
22+
title: [.enUS: "ECG"],
23+
subtitle: [.enUS: "Record an ECG using your Apple Watch"]
2324
)
2425
}

Sources/MHCStudyDefinitionExporter/Exporter.swift

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,23 @@ import SpeziStudyDefinition
1414

1515

1616
public enum Format: String, Codable, CaseIterable {
17-
case archive
17+
/// A zstd-compressed tar archive, readable on every platform. The default.
18+
case zstd
19+
/// The uncompressed bundle directory.
1820
case package
21+
/// An Apple Archive, readable only on Apple platforms.
22+
@available(*, deprecated, message: "Use the cross-platform 'zstd' format instead.")
23+
case archive
24+
25+
// Manual so the deprecated Apple Archive case neither breaks the synthesis nor
26+
// appears in the CLI's format suggestions.
27+
public static var allCases: [Format] {
28+
[.zstd, .package]
29+
}
1930
}
2031

2132

22-
/// Exports a `mhcStudyDefinition.spezistudybundle.aar` file to the specified `outputDir`.
33+
/// Exports the My Heart Counts study bundle to the specified `outputDir`, in the given ``Format``.
2334
@discardableResult
2435
public func export(to outputDir: URL, as format: Format) throws -> URL {
2536
let fileManager = FileManager.default
@@ -29,7 +40,7 @@ public func export(to outputDir: URL, as format: Format) throws -> URL {
2940
])
3041
}
3142
let filename = "mhcStudyBundle"
32-
let bundleUrl = outputDir.appendingPathComponent(filename, conformingTo: .speziStudyBundle)
43+
let bundleUrl = outputDir.appending(path: "\(filename).\(StudyBundle.fileExtension)", directoryHint: .isDirectory)
3344

3445
let inputFiles: [StudyBundle.FileResourceInput] = try Array {
3546
let bundleResourceUrl = try tryUnwrap(Bundle.module.resourceURL, "Unable to find Bundle /Resources URL")
@@ -61,17 +72,37 @@ public func export(to outputDir: URL, as format: Format) throws -> URL {
6172
)
6273
}
6374

64-
_ = try StudyBundle.writeToDisk(at: bundleUrl, definition: mhcStudyDefinition, files: inputFiles)
65-
75+
let bundle = try StudyBundle.writeToDisk(at: bundleUrl, definition: mhcStudyDefinition, files: inputFiles)
76+
6677
switch format {
6778
case .package:
6879
return bundleUrl
69-
case .archive:
70-
// Archive into .aar file
71-
let archiveUrl = bundleUrl.appendingPathExtension(for: .appleArchive)
80+
case .zstd:
81+
let archiveUrl = outputDir.appending(path: "\(filename).\(StudyBundle.archiveFileExtension)")
7282
try? fileManager.removeItem(at: archiveUrl)
73-
try fileManager.archiveDirectory(at: bundleUrl, to: archiveUrl)
83+
try bundle.archive(to: archiveUrl, compressionLevel: .maxRegular)
7484
try? fileManager.removeItem(at: bundleUrl)
7585
return archiveUrl
86+
case .archive:
87+
return try appleArchive(bundleAt: bundleUrl)
7688
}
7789
}
90+
91+
92+
/// Packages an exported bundle into an `.aar` file, replacing the bundle directory.
93+
///
94+
/// Apple Archive is only available on Apple platforms; everywhere else `.zstd` is the archive format.
95+
private func appleArchive(bundleAt bundleUrl: URL) throws -> URL {
96+
#if canImport(AppleArchive)
97+
let fileManager = FileManager.default
98+
let archiveUrl = bundleUrl.appendingPathExtension(for: .appleArchive)
99+
try? fileManager.removeItem(at: archiveUrl)
100+
try fileManager.archiveDirectory(at: bundleUrl, to: archiveUrl)
101+
try? fileManager.removeItem(at: bundleUrl)
102+
return archiveUrl
103+
#else
104+
throw NSError(domain: "edu.stanford.MHCStudyDefinitionExporter", code: 0, userInfo: [
105+
NSLocalizedDescriptionKey: "The Apple Archive format requires AppleArchive. Export with --format \(Format.zstd.rawValue)."
106+
])
107+
#endif
108+
}

Sources/MHCStudyDefinitionExporter/Resources/hhdExplainer/Sleep+en-UK.md renamed to Sources/MHCStudyDefinitionExporter/Resources/hhdExplainer/Sleep+en-GB.md

File renamed without changes.

Sources/MHCStudyDefinitionExporter/Study.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ extension StudyBundle.FileReference {
3939

4040

4141
let mhcStudyDefinition = StudyDefinition(
42-
studyRevision: 42,
42+
studyRevision: 43,
4343
metadata: .init(
4444
id: .mhcStudy,
4545
title: [.enUS: "My Heart Counts"],

Sources/MHCStudyDefinitionExporter/Utils/FileManager.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
// SPDX-License-Identifier: MIT
77
//
88

9+
#if canImport(AppleArchive)
910
import AppleArchive
1011
import Foundation
1112
import System
@@ -129,3 +130,5 @@ extension FileManager {
129130
}
130131
}
131132
}
133+
134+
#endif

Sources/MHCStudyDefinitionExporterCLI/CLI.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ struct Export: ParsableCommand {
3838
}
3939

4040
@Option(help: "The desired output format")
41-
var format: Format = .archive
41+
var format: Format = .zstd
4242

4343
@Argument(help: "Directory into which the output file should be stored.")
4444
var outputDir: String = "."
@@ -85,7 +85,7 @@ struct Validate: ParsableCommand {
8585
defer {
8686
try? fileManager.removeItem(at: dir)
8787
}
88-
try Export(format: .archive, outputDir: dir, isValidation: true).run()
88+
try Export(format: .zstd, outputDir: dir, isValidation: true).run()
8989
print("Validation succeeded.")
9090
}
9191
}

0 commit comments

Comments
 (0)