Compare commits

..

19 Commits

Author SHA1 Message Date
mahee96
dc29b65bd5 - fix: attempt to fix "app no longer available" issues due to fetch profiles not updating appFeatures and appGroups (possible regression from PR#846) 2025-03-15 01:58:08 +05:30
Zero King
1e64f50ab9 fix: typo in hasUpdate comparison
Signed-off-by: Zero King <l2dy@icloud.com>
2025-03-13 03:29:02 +05:30
mahee96
c8127fb3b9 - CI: updated stable.yml for recent fixes 2025-02-28 23:41:31 +05:30
Magesh K
729fca9100 Merge pull request #897 from SideStore/markdown-for-update-description
Feature: Render in-app update description as (full) markdown
2025-02-28 05:22:37 +05:30
mahee96
c6703d66c1 - Feature: Markdown view integration complete (if issues arrise can fix it asap) 2025-02-28 05:09:37 +05:30
mahee96
2197161d55 - Fix: CollapsingMarkdownView was not reloading properly during layoutSubviews phase 2025-02-28 02:28:34 +05:30
mahee96
cfaf79f878 - Fixes: disabled animations for the CollapsingMarkdownView when expanding/collapsing due to visual glitches 2025-02-28 01:02:41 +05:30
mahee96
2bea980d1f - Feature: Added markdown rendering for in-app update description field 2025-02-27 23:39:03 +05:30
mahee96
f11e27c712 - CI: serialize on whole workflow instead of individual jobs 2025-02-27 05:37:35 +05:30
mahee96
b316e84f0d - CI: serialize on whole workflow instead of individual jobs 2025-02-27 05:36:41 +05:30
mahee96
4668f8499b - CI: fix release notes updater for invalid revision as input 2025-02-27 05:23:20 +05:30
mahee96
f9aedaba04 - CI: fix release notes updater for invalid revision as input 2025-02-27 05:22:11 +05:30
mahee96
8cb3de9ab5 - CI: enable build on push for nightly temporarily 2025-02-27 05:03:42 +05:30
mahee96
ca57d58219 - CI: fix: deploy errors 2025-02-27 05:01:27 +05:30
mahee96
6a56fbd206 -CI: fix: deploy must run if tests were sccessful or skipped 2025-02-27 04:57:34 +05:30
mahee96
cec3825de0 - Makefile: build-tests need destination param same as run-tests 2025-02-27 04:44:35 +05:30
mahee96
b3e99d1ae3 - CI: Try composite build 2025-02-27 04:30:45 +05:30
mahee96
7243d79646 - UITests: Fix issue with UITests bundleID causing test failures in CI 2025-02-27 02:50:16 +05:30
Magesh K
e50da6603c - CI: replaced irgaly/xcode-cache with gh-actions-cache
- CI: serialization bug fix - reverted concurrency lock to workflow level instead of job level which was causing issues due to incorrect cache at expected jobs
- CI: Boot simulator in async instead of doing in a blocking step
- CI: integrate publishing release notes
- CI: moved posting to beta-build-num repo into build-job instead of deploy coz we want to roll the beta-build-num for each success of build-job even if the workflow failed.
2025-02-27 02:49:20 +05:30
26 changed files with 2154 additions and 208 deletions

View File

@@ -45,7 +45,7 @@ on:
required: false
jobs:
common:
serialize:
name: Wait for other jobs
# since build cache, test-build cache, test-run cache are involved, out of order exec if serialization is on individual jobs will wreak all sorts of havoc
# so we serialize on the entire workflow
@@ -68,13 +68,13 @@ jobs:
build:
name: Build SideStore - ${{ inputs.release_tag }}
needs: common
needs: serialize
strategy:
fail-fast: false
matrix:
include:
- os: 'macos-15'
version: '16.1'
version: '16.2'
runs-on: ${{ matrix.os }}
outputs:
@@ -160,7 +160,7 @@ jobs:
build_num=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/.*\.([0-9]+)\+.*/\1/')
# Combine them into the final output
MARKETING_VERSION="${version}-${date}.${build_num}+${{ needs.common.outputs.short-commit }}"
MARKETING_VERSION="${version}-${date}.${build_num}+${{ needs.serialize.outputs.short-commit }}"
echo "MARKETING_VERSION=$MARKETING_VERSION" >> $GITHUB_ENV
echo "MARKETING_VERSION=$MARKETING_VERSION" >> $GITHUB_OUTPUT
@@ -177,14 +177,32 @@ jobs:
with:
xcode-version: ${{ matrix.version }}
- name: (Build) Cache Build
uses: irgaly/xcode-cache@v1
- name: (Build) Restore Xcode & SwiftPM Cache (Exact match)
id: xcode-cache-restore
uses: actions/cache/restore@v3
with:
key: xcode-cache-deriveddata-build-${{ github.ref_name }}-${{ github.sha }}
restore-keys: xcode-cache-deriveddata-build-${{ github.ref_name }}-
swiftpm-cache-key: xcode-cache-sourcedata-build-${{ github.ref_name }}-${{ github.sha }}
swiftpm-cache-restore-keys: |
xcode-cache-sourcedata-build-${{ github.ref_name }}-
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-${{ github.ref_name }}-${{ github.sha }}
- name: (Build) Restore Xcode & SwiftPM Cache (Last Available)
id: xcode-cache-restore-recent
uses: actions/cache/restore@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-${{ github.ref_name }}-
# - name: (Build) Cache Build
# uses: irgaly/xcode-cache@v1.8.1
# with:
# key: xcode-cache-deriveddata-build-${{ github.ref_name }}-${{ github.sha }}
# restore-keys: xcode-cache-deriveddata-build-${{ github.ref_name }}-
# swiftpm-cache-key: xcode-cache-sourcedata-build-${{ github.ref_name }}-${{ github.sha }}
# swiftpm-cache-restore-keys: |
# xcode-cache-sourcedata-build-${{ github.ref_name }}-
- name: (Build) Restore Pods from Cache (Exact match)
id: pods-restore
@@ -269,6 +287,16 @@ jobs:
- name: Convert to IPA
run: make ipa | tee -a build/logs/build.log
- name: (Build) Save Xcode & SwiftPM Cache
id: cache-save
if: ${{ steps.xcode-cache-restore.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-${{ github.ref_name }}-${{ github.sha }}
- name: (Build) List Files and Build artifacts
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
@@ -287,6 +315,10 @@ jobs:
find SideStore.xcarchive -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
echo ""
- name: Encrypt build-logs for upload
id: encrypt-build-log
run: |
@@ -336,7 +368,7 @@ jobs:
echo "Adding files to commit"
git add --verbose build_number.txt
git commit -m " - updated for ${{ inputs.release_tag }} - ${{ needs.common.outputs.short-commit }} deployment" || echo "No changes to commit"
git commit -m " - updated for ${{ inputs.release_tag }} - ${{ needs.serialize.outputs.short-commit }} deployment" || echo "No changes to commit"
echo "Pushing to remote repo"
git push --verbose
@@ -371,30 +403,31 @@ jobs:
- name: Upload release-notes.md
uses: actions/upload-artifact@v4
with:
name: release-notes-${{ needs.common.outputs.short-commit }}.md
name: release-notes-${{ needs.serialize.outputs.short-commit }}.md
path: release-notes.md
- name: Upload update_release_notes.py
uses: actions/upload-artifact@v4
with:
name: update_release_notes-${{ needs.common.outputs.short-commit }}.py
name: update_release_notes-${{ needs.serialize.outputs.short-commit }}.py
path: update_release_notes.py
- name: Upload update_apps.py
uses: actions/upload-artifact@v4
with:
name: update_apps-${{ needs.common.outputs.short-commit }}.py
name: update_apps-${{ needs.serialize.outputs.short-commit }}.py
path: update_apps.py
tests-build:
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }}
name: Tests-Build SideStore - ${{ inputs.release_tag }}
needs: common
needs: serialize
strategy:
fail-fast: false
matrix:
include:
- os: 'macos-15'
version: '16.1'
version: '16.2'
runs-on: ${{ matrix.os }}
steps:
@@ -410,16 +443,36 @@ jobs:
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: '16.1'
xcode-version: '16.2'
- name: (Tests-Build) Cache Build
uses: irgaly/xcode-cache@v1
# - name: (Tests-Build) Cache Build
# uses: irgaly/xcode-cache@v1.8.1
# with:
# key: xcode-cache-deriveddata-test-${{ github.ref_name }}-${{ github.sha }}
# # tests shouldn't restore cache unless it is same build
# # restore-keys: xcode-cache-deriveddata-test-${{ github.ref_name }}-
# swiftpm-cache-key: xcode-cache-sourcedata-test-${{ github.ref_name }}-${{ github.sha }}
# swiftpm-cache-restore-keys: |
# xcode-cache-sourcedata-test-${{ github.ref_name }}-
# delete-used-deriveddata-cache: true
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Exact match)
id: xcode-cache-restore
uses: actions/cache/restore@v3
with:
key: xcode-cache-deriveddata-test-${{ github.ref_name }}-${{ github.sha }}
restore-keys: xcode-cache-deriveddata-test-${{ github.ref_name }}-
swiftpm-cache-key: xcode-cache-sourcedata-test-${{ github.ref_name }}-${{ github.sha }}
swiftpm-cache-restore-keys: |
xcode-cache-sourcedata-test-${{ github.ref_name }}-
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Last Available)
id: xcode-cache-restore-recent
uses: actions/cache/restore@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-tests-${{ github.ref_name }}-
- name: (Tests-Build) Restore Pods from Cache (Exact match)
id: pods-restore
@@ -455,6 +508,13 @@ jobs:
./AltStore.xcworkspace/
key: pods-cache-test-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
- name: Clean Derived Data (if required)
if: ${{ vars.PERFORM_CLEAN_TESTS_BUILD == '1' }}
run: |
rm -rf ~/Library/Developer/Xcode/DerivedData/
make clean
xcodebuild clean
- name: (Tests-Build) Clean previous build artifacts
run: |
make clean
@@ -487,6 +547,16 @@ jobs:
run: |
NSUnbufferedIO=YES make -B build-tests 2>&1 | tee -a build/logs/tests-build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
- name: (Tests-Build) Save Xcode & SwiftPM Cache
id: cache-save
if: ${{ steps.xcode-cache-restore.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
- name: (Tests-Build) List Files and Build artifacts
if: always()
run: |
@@ -498,6 +568,16 @@ jobs:
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
find ~/Library/Developer/Xcode/DerivedData -maxdepth 8 -exec ls -ld {} + | grep "Build/Products" >> tests-build-deriveddata.txt || true
echo ""
- uses: actions/upload-artifact@v4
if: always()
with:
name: tests-build-deriveddata-${{ needs.serialize.outputs.short-commit }}.txt
path: tests-build-deriveddata.txt
- name: Encrypt tests-build-logs for upload
id: encrypt-test-log
if: always()
@@ -519,18 +599,19 @@ jobs:
if: always() && steps.encrypt-test-log.outputs.encrypted == 'true'
uses: actions/upload-artifact@v4
with:
name: encrypted-tests-build-logs-${{ needs.common.outputs.short-commit }}.zip
name: encrypted-tests-build-logs-${{ needs.serialize.outputs.short-commit }}.zip
path: encrypted-tests-build-logs.zip
tests-run:
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
name: Tests-Run SideStore - ${{ inputs.release_tag }}
needs: [common, tests-build]
needs: [serialize, tests-build]
strategy:
fail-fast: false
matrix:
include:
- os: 'macos-15'
version: '16.1'
version: '16.2'
runs-on: ${{ matrix.os }}
steps:
@@ -547,16 +628,23 @@ jobs:
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: '16.1'
xcode-version: '16.2'
- name: (Tests-Run) Cache Build
uses: irgaly/xcode-cache@v1
# - name: (Tests-Run) Cache Build
# uses: irgaly/xcode-cache@v1.8.1
# with:
# # This comes from
# key: xcode-cache-deriveddata-test-${{ github.ref_name }}-${{ github.sha }}
# swiftpm-cache-key: xcode-cache-sourcedata-test-${{ github.ref_name }}-${{ github.sha }}
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Exact match) [from tests-build job]
id: xcode-cache-restore
uses: actions/cache/restore@v3
with:
key: xcode-cache-deriveddata-test-${{ github.ref_name }}-${{ github.sha }}
restore-keys: xcode-cache-deriveddata-test-${{ github.ref_name }}-
swiftpm-cache-key: xcode-cache-sourcedata-test-${{ github.ref_name }}-${{ github.sha }}
swiftpm-cache-restore-keys: |
xcode-cache-sourcedata-test-${{ github.ref_name }}-
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
- name: (Tests-Run) Restore Pods from Cache (Exact match)
id: pods-restore
@@ -616,9 +704,15 @@ jobs:
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
find ~/Library/Developer/Xcode/DerivedData -maxdepth 8 -exec ls -ld {} + | grep "Build/Products" >> tests-run-deriveddata.txt || true
echo ""
- uses: actions/upload-artifact@v4
if: always()
with:
name: tests-run-deriveddata-${{ needs.serialize.outputs.short-commit }}.txt
path: tests-run-deriveddata.txt
# we expect simulator to have been booted by now, so exit otherwise
- name: Simulator Boot Check
run: |
@@ -676,7 +770,7 @@ jobs:
if: always() && steps.encrypt-test-log.outputs.encrypted == 'true'
uses: actions/upload-artifact@v4
with:
name: encrypted-tests-run-logs-${{ needs.common.outputs.short-commit }}.zip
name: encrypted-tests-run-logs-${{ needs.serialize.outputs.short-commit }}.zip
path: encrypted-tests-run-logs.zip
- name: Print tests-recording.log contents (if exists)
@@ -706,7 +800,7 @@ jobs:
if: ${{ always() && steps.check-recording.outputs.found == 'true' }}
uses: actions/upload-artifact@v4
with:
name: tests-recording-${{ needs.common.outputs.short-commit }}.mp4
name: tests-recording-${{ needs.serialize.outputs.short-commit }}.mp4
path: tests-recording.mp4
- name: Zip test-results
@@ -715,14 +809,14 @@ jobs:
- name: Upload Test Artifacts
uses: actions/upload-artifact@v4
with:
name: test-results-${{ needs.common.outputs.short-commit }}.zip
name: test-results-${{ needs.serialize.outputs.short-commit }}.zip
path: test-results.zip
deploy:
name: Deploy SideStore - ${{ inputs.release_tag }}
runs-on: macos-15
# needs: [common, build]
needs: [common, build, tests-build, tests-run]
# needs: [serialize, build]
needs: [serialize, build, tests-build, tests-run]
steps:
- name: Download IPA artifact
uses: actions/download-artifact@v4
@@ -740,39 +834,43 @@ jobs:
name: encrypted-build-logs-${{ needs.build.outputs.version }}.zip
- name: Download encrypted-tests-build-logs artifact
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }}
uses: actions/download-artifact@v4
with:
name: encrypted-tests-build-logs-${{ needs.common.outputs.short-commit }}.zip
name: encrypted-tests-build-logs-${{ needs.serialize.outputs.short-commit }}.zip
- name: Download encrypted-tests-run-logs artifact
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
uses: actions/download-artifact@v4
with:
name: encrypted-tests-run-logs-${{ needs.common.outputs.short-commit }}.zip
name: encrypted-tests-run-logs-${{ needs.serialize.outputs.short-commit }}.zip
- name: Download tests-recording artifact
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
uses: actions/download-artifact@v4
with:
name: tests-recording-${{ needs.common.outputs.short-commit }}.mp4
name: tests-recording-${{ needs.serialize.outputs.short-commit }}.mp4
- name: Download test-results artifact
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
uses: actions/download-artifact@v4
with:
name: test-results-${{ needs.common.outputs.short-commit }}.zip
name: test-results-${{ needs.serialize.outputs.short-commit }}.zip
- name: Download release-notes.md
uses: actions/download-artifact@v4
with:
name: release-notes-${{ needs.common.outputs.short-commit }}.md
name: release-notes-${{ needs.serialize.outputs.short-commit }}.md
- name: Download update_release_notes.py
uses: actions/download-artifact@v4
with:
name: update_release_notes-${{ needs.common.outputs.short-commit }}.py
name: update_release_notes-${{ needs.serialize.outputs.short-commit }}.py
- name: Download update_apps.py
uses: actions/download-artifact@v4
with:
name: update_apps-${{ needs.common.outputs.short-commit }}.py
name: update_apps-${{ needs.serialize.outputs.short-commit }}.py
- name: Read release notes
id: release_notes
@@ -850,7 +948,7 @@ jobs:
LOCALIZED_DESCRIPTION=$(cat <<EOF
This is release for:
- version: "${{ needs.build.outputs.version }}"
- revision: "${{ needs.common.outputs.short-commit }}"
- revision: "${{ needs.serialize.outputs.short-commit }}"
- timestamp: "${{ steps.date.outputs.date }}"
Release Notes:
@@ -903,7 +1001,7 @@ jobs:
# Commit changes and push using SSH
git add --verbose ./_includes/source.json
git commit -m " - updated for ${{ needs.common.outputs.short-commit }} deployment" || echo "No changes to commit"
git commit -m " - updated for ${{ needs.serialize.outputs.short-commit }} deployment" || echo "No changes to commit"
git push --verbose
popd

63
.github/maintenance/cache.py vendored Normal file
View File

@@ -0,0 +1,63 @@
import requests
import sys
import os
# Your GitHub Personal Access Token
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
# Repository details
REPO_OWNER = "SideStore"
REPO_NAME = "SideStore"
API_URL = f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/actions/caches"
# Common headers for GitHub API calls
HEADERS = {
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {GITHUB_TOKEN}"
}
def list_caches():
response = requests.get(API_URL, headers=HEADERS)
if response.status_code != 200:
print(f"Failed to list caches. HTTP {response.status_code}")
print("Response:", response.text)
sys.exit(1)
data = response.json()
return data.get("actions_caches", [])
def delete_cache(cache_id):
delete_url = f"{API_URL}/{cache_id}"
response = requests.delete(delete_url, headers=HEADERS)
return response.status_code
def main():
caches = list_caches()
if not caches:
print("No caches found.")
return
print("Found caches:")
for cache in caches:
print(f"ID: {cache.get('id')}, Key: {cache.get('key')}")
print("\nDeleting caches...")
for cache in caches:
cache_id = cache.get("id")
status = delete_cache(cache_id)
if status == 204:
print(f"Successfully deleted cache with ID: {cache_id}")
else:
print(f"Failed to delete cache with ID: {cache_id}. HTTP status code: {status}")
print("All caches processed.")
if __name__ == "__main__":
main()
### How to use
'''
just export the GITHUB_TOKEN and then run this script via `python3 cache.py' to delete the caches
'''

View File

@@ -10,8 +10,8 @@ concurrency:
cancel-in-progress: true
jobs:
Reuseable-build:
uses: ./.github/workflows/reusable-build-workflow.yml
Reusable-build:
uses: ./.github/workflows/reusable-sidestore-build.yml
with:
# bundle_id: "com.SideStore.SideStore.Alpha"
bundle_id: "com.SideStore.SideStore"

View File

@@ -58,13 +58,13 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
LAST_SUCCESS: ${{ env.last_success }}
Reuseable-build:
Reusable-build:
if: |
always() &&
(github.event_name == 'push' ||
(github.event_name == 'schedule' && needs.check-changes.result == 'success' && needs.check-changes.outputs.has_changes == 'true'))
needs: check-changes
uses: ./.github/workflows/reusable-build-workflow.yml
uses: ./.github/workflows/reusable-sidestore-build.yml
with:
# bundle_id: "com.SideStore.SideStore.Nightly"
bundle_id: "com.SideStore.SideStore"

View File

@@ -0,0 +1,104 @@
name: Reusable SideStore Build
on:
workflow_call:
inputs:
is_beta:
required: false
default: false
type: boolean
publish:
required: false
default: false
type: boolean
is_shared_build_num:
required: false
default: true
type: boolean
release_name:
required: true
type: string
release_tag:
required: true
type: string
upstream_tag:
required: true
type: string
upstream_name:
required: true
type: string
bundle_id:
default: com.SideStore.SideStore
required: true
type: string
bundle_id_suffix:
default: ''
required: false
type: string
secrets:
# GITHUB_TOKEN:
# required: true
CROSS_REPO_PUSH_KEY:
required: true
BUILD_LOG_ZIP_PASSWORD:
required: false
# since build cache, test-build cache, test-run cache are involved, out of order exec if serialization is on individual jobs will wreak all sorts of havoc
# so we serialize on the entire workflow
concurrency:
group: serialize-workflow
jobs:
shared:
uses: ./.github/workflows/sidestore-shared.yml
secrets: inherit
build:
needs: shared
uses: ./.github/workflows/sidestore-build.yml
with:
is_beta: ${{ inputs.is_beta }}
is_shared_build_num: ${{ inputs.is_shared_build_num }}
release_tag: ${{ inputs.release_tag }}
short_commit: ${{ needs.shared.outputs.short-commit }}
bundle_id: ${{ inputs.bundle_id }}
bundle_id_suffix: ${{ inputs.bundle_id_suffix }}
secrets: inherit
tests-build:
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }}
needs: shared
uses: ./.github/workflows/sidestore-tests-build.yml
with:
release_tag: ${{ inputs.release_tag }}
short_commit: ${{ needs.shared.outputs.short-commit }}
secrets: inherit
tests-run:
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
needs: [shared, tests-build]
uses: ./.github/workflows/sidestore-tests-run.yml
with:
release_tag: ${{ inputs.release_tag }}
short_commit: ${{ needs.shared.outputs.short-commit }}
secrets: inherit
deploy:
needs: [shared, build, tests-build, tests-run] # Keep tests-run in needs
if: ${{ always() && (needs.tests-run.result == 'skipped' || needs.tests-run.result == 'success') }}
uses: ./.github/workflows/sidestore-deploy.yml
with:
is_beta: ${{ inputs.is_beta }}
publish: ${{ inputs.publish }}
release_name: ${{ inputs.release_name }}
release_tag: ${{ inputs.release_tag }}
upstream_tag: ${{ inputs.upstream_tag }}
upstream_name: ${{ inputs.upstream_name }}
version: ${{ needs.build.outputs.version }}
short_commit: ${{ needs.shared.outputs.short-commit }}
release_channel: ${{ needs.build.outputs.release-channel }}
marketing_version: ${{ needs.build.outputs.marketing-version }}
bundle_id: ${{ inputs.bundle_id }}
secrets: inherit

401
.github/workflows/sidestore-build.yml vendored Normal file
View File

@@ -0,0 +1,401 @@
name: SideStore Build
on:
workflow_call:
inputs:
is_beta:
type: boolean
is_shared_build_num:
type: boolean
release_tag:
type: string
bundle_id:
type: string
bundle_id_suffix:
type: string
short_commit:
type: string
secrets:
CROSS_REPO_PUSH_KEY:
required: true
BUILD_LOG_ZIP_PASSWORD:
required: false
outputs:
version:
value: ${{ jobs.build.outputs.version }}
marketing-version:
value: ${{ jobs.build.outputs.marketing-version }}
release-channel:
value: ${{ jobs.build.outputs.release-channel }}
jobs:
build:
name: Build SideStore - ${{ inputs.release_tag }}
strategy:
fail-fast: false
matrix:
include:
- os: 'macos-15'
version: '16.2'
runs-on: ${{ matrix.os }}
outputs:
version: ${{ steps.version.outputs.version }}
marketing-version: ${{ steps.marketing-version.outputs.MARKETING_VERSION }}
release-channel: ${{ steps.release-channel.outputs.RELEASE_CHANNEL }}
steps:
- name: Set beta status
run: echo "IS_BETA=${{ inputs.is_beta }}" >> $GITHUB_ENV
shell: bash
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Install dependencies - ldid & xcbeautify
run: |
brew install ldid xcbeautify
- name: Set ref based on is_shared_build_num
if: ${{ inputs.is_beta }}
id: set_ref
run: |
if [ "${{ inputs.is_shared_build_num }}" == "true" ]; then
echo "ref=main" >> $GITHUB_ENV
else
echo "ref=${{ inputs.release_tag }}" >> $GITHUB_ENV
fi
shell: bash
- name: Checkout SideStore/beta-build-num repo
if: ${{ inputs.is_beta }}
uses: actions/checkout@v4
with:
repository: 'SideStore/beta-build-num'
ref: ${{ env.ref }}
token: ${{ secrets.CROSS_REPO_PUSH_KEY }}
path: 'SideStore/beta-build-num'
- name: Copy build_number.txt to repo root
if: ${{ inputs.is_beta }}
run: |
cp SideStore/beta-build-num/build_number.txt .
echo "cat build_number.txt"
cat build_number.txt
shell: bash
- name: Echo Build.xcconfig
run: |
echo "cat Build.xcconfig"
cat Build.xcconfig
shell: bash
- name: Set Release Channel info for build number bumper
id: release-channel
run: |
RELEASE_CHANNEL="${{ inputs.release_tag }}"
echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}" >> $GITHUB_ENV
echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}" >> $GITHUB_OUTPUT
echo "RELEASE_CHANNEL=${RELEASE_CHANNEL}"
shell: bash
- name: Increase build number for beta builds
if: ${{ inputs.is_beta }}
run: |
bash .github/workflows/increase-beta-build-num.sh
shell: bash
- name: Extract MARKETING_VERSION from Build.xcconfig
id: version
run: |
version=$(grep MARKETING_VERSION Build.xcconfig | sed -e 's/MARKETING_VERSION = //g')
echo "version=$version" >> $GITHUB_OUTPUT
echo "version=$version"
shell: bash
- name: Set MARKETING_VERSION
if: ${{ inputs.is_beta }}
id: marketing-version
run: |
# Extract version number (e.g., "0.6.0")
version=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/^[^0-9]*([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
# Extract date (YYYYMMDD) (e.g., "20250205")
date=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/.*\.([0-9]{4})\.([0-9]{2})\.([0-9]{2})\..*/\1\2\3/')
# Extract build number (e.g., "2")
build_num=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/.*\.([0-9]+)\+.*/\1/')
# Combine them into the final output
MARKETING_VERSION="${version}-${date}.${build_num}+${{ inputs.short_commit }}"
echo "MARKETING_VERSION=$MARKETING_VERSION" >> $GITHUB_ENV
echo "MARKETING_VERSION=$MARKETING_VERSION" >> $GITHUB_OUTPUT
echo "MARKETING_VERSION=$MARKETING_VERSION"
shell: bash
- name: Echo Updated Build.xcconfig, build_number.txt
if: ${{ inputs.is_beta }}
run: |
cat Build.xcconfig
cat build_number.txt
shell: bash
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: ${{ matrix.version }}
- name: (Build) Restore Xcode & SwiftPM Cache (Exact match)
id: xcode-cache-restore
uses: actions/cache/restore@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-${{ github.ref_name }}-${{ github.sha }}
- name: (Build) Restore Xcode & SwiftPM Cache (Last Available)
id: xcode-cache-restore-recent
uses: actions/cache/restore@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-${{ github.ref_name }}-
# - name: (Build) Cache Build
# uses: irgaly/xcode-cache@v1.8.1
# with:
# key: xcode-cache-deriveddata-build-${{ github.ref_name }}-${{ github.sha }}
# restore-keys: xcode-cache-deriveddata-build-${{ github.ref_name }}-
# swiftpm-cache-key: xcode-cache-sourcedata-build-${{ github.ref_name }}-${{ github.sha }}
# swiftpm-cache-restore-keys: |
# xcode-cache-sourcedata-build-${{ github.ref_name }}-
- name: (Build) Restore Pods from Cache (Exact match)
id: pods-restore
uses: actions/cache/restore@v3
with:
path: |
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-build-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
# restore-keys: | # commented out to strictly check cache for this particular podfile
# pods-cache-
- name: (Build) Restore Pods from Cache (Last Available)
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
id: pods-restore-recent
uses: actions/cache/restore@v3
with:
path: |
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-build-${{ github.ref_name }}-
- name: (Build) Install CocoaPods
run: pod install
shell: bash
- name: (Build) Save Pods to Cache
id: save-pods
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v3
with:
path: |
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-build-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
- name: (Build) Clean previous build artifacts
# using 'tee' to intercept stdout and log for detailed build-log
run: |
make clean
mkdir -p build/logs
shell: bash
- name: (Build) List Files and derived data
if: always()
shell: bash
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
echo ">>>>>>>>> Pods <<<<<<<<<<"
find Pods -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> SideStore <<<<<<<<<<"
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
echo ""
- name: Set BundleID Suffix for Sidestore build
run: |
echo "BUNDLE_ID_SUFFIX=${{ inputs.bundle_id_suffix }}" >> $GITHUB_ENV
shell: bash
- name: Build SideStore.xcarchive
# using 'tee' to intercept stdout and log for detailed build-log
run: |
NSUnbufferedIO=YES make -B build 2>&1 | tee -a build/logs/build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
shell: bash
- name: Fakesign app
run: make fakesign | tee -a build/logs/build.log
shell: bash
- name: Convert to IPA
run: make ipa | tee -a build/logs/build.log
shell: bash
- name: (Build) Save Xcode & SwiftPM Cache
id: cache-save
if: ${{ steps.xcode-cache-restore.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-${{ github.ref_name }}-${{ github.sha }}
- name: (Build) List Files and Build artifacts
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
echo ">>>>>>>>> Build <<<<<<<<<<"
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> SideStore <<<<<<<<<<"
find SideStore -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> SideStore.xcarchive <<<<<<<<<<"
find SideStore.xcarchive -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
echo ""
shell: bash
- name: Encrypt build-logs for upload
id: encrypt-build-log
run: |
DEFAULT_BUILD_LOG_PASSWORD=12345
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
fi
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-build-logs.zip * || popd
echo "::set-output name=encrypted::true"
shell: bash
- name: Upload encrypted-build-logs.zip
id: attach-encrypted-build-log
if: ${{ always() && steps.encrypt-build-log.outputs.encrypted == 'true' }}
uses: actions/upload-artifact@v4
with:
name: encrypted-build-logs-${{ steps.version.outputs.version }}.zip
path: encrypted-build-logs.zip
- name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v4
with:
name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore.ipa
- name: Zip dSYMs
run: zip -r -9 ./SideStore.dSYMs.zip ./SideStore.xcarchive/dSYMs
shell: bash
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v4
with:
name: SideStore-${{ steps.version.outputs.version }}-dSYMs.zip
path: SideStore.dSYMs.zip
- name: Keep rolling the build numbers for each successful build
if: ${{ inputs.is_beta }}
run: |
pushd SideStore/beta-build-num/
echo "Configure Git user (committer details)"
git config user.name "GitHub Actions"
git config user.email "github-actions@github.com"
echo "Adding files to commit"
git add --verbose build_number.txt
git commit -m " - updated for ${{ inputs.release_tag }} - ${{ inputs.short_commit }} deployment" || echo "No changes to commit"
echo "Pushing to remote repo"
git push --verbose
popd
shell: bash
- name: Get last successful commit
id: get_last_commit
run: |
# Try to get the last successful workflow run commit
LAST_SUCCESS_SHA=$(gh run list --branch "${{ github.ref_name }}" --status success --json headSha --jq '.[0].headSha')
echo "LAST_SUCCESS_SHA=$LAST_SUCCESS_SHA" >> $GITHUB_OUTPUT
echo "LAST_SUCCESS_SHA=$LAST_SUCCESS_SHA" >> $GITHUB_ENV
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
- name: Create release notes
run: |
LAST_SUCCESS_SHA=${{ steps.get_last_commit.outputs.LAST_SUCCESS_SHA}}
echo "Last successful commit SHA: $LAST_SUCCESS_SHA"
FROM_COMMIT=$LAST_SUCCESS_SHA
# Check if we got a valid SHA
if [ -z "$LAST_SUCCESS_SHA" ] || [ "$LAST_SUCCESS_SHA" = "null" ]; then
echo "No successful run found, using initial commit of branch"
# Get the first commit of the branch (initial commit)
FROM_COMMIT=$(git rev-list --max-parents=0 HEAD)
fi
python3 update_release_notes.py $FROM_COMMIT ${{ inputs.release_tag }} ${{ github.ref_name }}
# cat release-notes.md
shell: bash
- name: Upload release-notes.md
uses: actions/upload-artifact@v4
with:
name: release-notes-${{ inputs.short_commit }}.md
path: release-notes.md
- name: Upload update_release_notes.py
uses: actions/upload-artifact@v4
with:
name: update_release_notes-${{ inputs.short_commit }}.py
path: update_release_notes.py
- name: Upload update_apps.py
uses: actions/upload-artifact@v4
with:
name: update_apps-${{ inputs.short_commit }}.py
path: update_apps.py

235
.github/workflows/sidestore-deploy.yml vendored Normal file
View File

@@ -0,0 +1,235 @@
name: SideStore Deploy
on:
workflow_call:
inputs:
is_beta:
type: boolean
publish:
type: boolean
release_name:
type: string
release_tag:
type: string
upstream_tag:
type: string
upstream_name:
type: string
version:
type: string
short_commit:
type: string
marketing_version:
type: string
release_channel:
type: string
bundle_id:
type: string
secrets:
CROSS_REPO_PUSH_KEY:
required: true
# GITHUB_TOKEN:
# required: true
jobs:
deploy:
name: Deploy SideStore - ${{ inputs.release_tag }}
runs-on: macos-15
steps:
- name: Download IPA artifact
uses: actions/download-artifact@v4
with:
name: SideStore-${{ inputs.version }}.ipa
- name: Download dSYM artifact
uses: actions/download-artifact@v4
with:
name: SideStore-${{ inputs.version }}-dSYMs.zip
- name: Download encrypted-build-logs artifact
uses: actions/download-artifact@v4
with:
name: encrypted-build-logs-${{ inputs.version }}.zip
- name: Download encrypted-tests-build-logs artifact
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }}
uses: actions/download-artifact@v4
with:
name: encrypted-tests-build-logs-${{ inputs.short_commit }}.zip
- name: Download encrypted-tests-run-logs artifact
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
uses: actions/download-artifact@v4
with:
name: encrypted-tests-run-logs-${{ inputs.short_commit }}.zip
- name: Download tests-recording artifact
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
uses: actions/download-artifact@v4
with:
name: tests-recording-${{ inputs.short_commit }}.mp4
- name: Download test-results artifact
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
uses: actions/download-artifact@v4
with:
name: test-results-${{ inputs.short_commit }}.zip
- name: Download release-notes.md
uses: actions/download-artifact@v4
with:
name: release-notes-${{ inputs.short_commit }}.md
- name: Download update_release_notes.py
uses: actions/download-artifact@v4
with:
name: update_release_notes-${{ inputs.short_commit }}.py
- name: Download update_apps.py
uses: actions/download-artifact@v4
with:
name: update_apps-${{ inputs.short_commit }}.py
- name: Read release notes
id: release_notes
run: |
CONTENT=$(python3 update_release_notes.py --retrieve ${{ inputs.release_tag }})
echo "content<<EOF" >> $GITHUB_OUTPUT
echo "$CONTENT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
shell: bash
- name: List files before upload
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
find . -maxdepth 4 -exec ls -ld {} + || true # List contents if directory exists
echo ""
shell: bash
- name: Get current date
id: date
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
shell: bash
- name: Get current date in AltStore date form
id: date_altstore
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
shell: bash
- name: Upload to releases
uses: IsaacShelton/update-existing-release@v1.3.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
release: ${{ inputs.release_name }}
tag: ${{ inputs.release_tag }}
prerelease: ${{ inputs.is_beta }}
files: SideStore.ipa SideStore.dSYMs.zip encrypted-build-logs.zip encrypted-tests-build-logs.zip encrypted-tests-run-logs.zip test-results.zip tests-recording.mp4
body: |
This is an ⚠️ **EXPERIMENTAL** ⚠️ ${{ inputs.release_name }} build for commit [${{ github.sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}).
${{ inputs.release_name }} builds are **extremely experimental builds only meant to be used by developers and beta testers. They often contain bugs and experimental features. Use at your own risk!**
If you want to try out new features early but want a lower chance of bugs, you can look at [SideStore ${{ inputs.upstream_name }}](https://github.com/${{ github.repository }}/releases?q=${{ inputs.upstream_tag }}).
## Build Info
Built at (UTC): `${{ steps.date.outputs.date }}`
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
Commit SHA: `${{ github.sha }}`
Version: `${{ inputs.version }}`
${{ steps.release_notes.outputs.content }}
- name: Get formatted date
run: |
FORMATTED_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
echo "Formatted date: $FORMATTED_DATE"
echo "FORMATTED_DATE=$FORMATTED_DATE" >> $GITHUB_ENV
shell: bash
- name: Get size of IPA in bytes (macOS/Linux)
run: |
if [[ "$(uname)" == "Darwin" ]]; then
# macOS
IPA_SIZE=$(stat -f %z SideStore.ipa)
else
# Linux
IPA_SIZE=$(stat -c %s SideStore.ipa)
fi
echo "IPA size in bytes: $IPA_SIZE"
echo "IPA_SIZE=$IPA_SIZE" >> $GITHUB_ENV
shell: bash
- name: Compute SHA-256 of IPA
run: |
SHA256_HASH=$(shasum -a 256 SideStore.ipa | awk '{ print $1 }')
echo "SHA-256 Hash: $SHA256_HASH"
echo "SHA256_HASH=$SHA256_HASH" >> $GITHUB_ENV
shell: bash
- name: Set Release Info variables
run: |
# Format localized description
LOCALIZED_DESCRIPTION=$(cat <<EOF
This is release for:
- version: "${{ inputs.version }}"
- revision: "${{ inputs.short_commit }}"
- timestamp: "${{ steps.date.outputs.date }}"
Release Notes:
${{ steps.release_notes.outputs.content }}
EOF
)
echo "IS_BETA=${{ inputs.is_beta }}" >> $GITHUB_ENV
echo "BUNDLE_IDENTIFIER=${{ inputs.bundle_id }}" >> $GITHUB_ENV
echo "VERSION_IPA=${{ inputs.marketing_version }}" >> $GITHUB_ENV
echo "VERSION_DATE=$FORMATTED_DATE" >> $GITHUB_ENV
echo "RELEASE_CHANNEL=${{ inputs.release_channel }}" >> $GITHUB_ENV
echo "SIZE=$IPA_SIZE" >> $GITHUB_ENV
echo "SHA256=$SHA256_HASH" >> $GITHUB_ENV
echo "DOWNLOAD_URL=https://github.com/SideStore/SideStore/releases/download/${{ inputs.release_tag }}/SideStore.ipa" >> $GITHUB_ENV
# multiline strings
echo "LOCALIZED_DESCRIPTION<<EOF" >> $GITHUB_ENV
echo "$LOCALIZED_DESCRIPTION" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
shell: bash
- name: Check if Publish updates is set
id: check_publish
run: |
echo "Publish updates to source.json = ${{ inputs.publish }}"
shell: bash
- name: Checkout SideStore/apps-v2.json
if: ${{ inputs.is_beta && inputs.publish }}
uses: actions/checkout@v4
with:
repository: 'SideStore/apps-v2.json'
ref: 'main' # this branch is shared by all beta builds, so beta build workflows are serialized
token: ${{ secrets.CROSS_REPO_PUSH_KEY }}
path: 'SideStore/apps-v2.json'
# for stable builds, let the user manually edit the source.json
- name: Publish to SideStore/apps-v2.json
if: ${{ inputs.is_beta && inputs.publish }}
id: publish-release
shell: bash
run: |
# Copy and execute the update script
pushd SideStore/apps-v2.json/
# Configure Git user (committer details)
git config user.name "GitHub Actions"
git config user.email "github-actions@github.com"
# update the source.json
python3 ../../update_apps.py "./_includes/source.json"
# Commit changes and push using SSH
git add --verbose ./_includes/source.json
git commit -m " - updated for ${{ inputs.short_commit }} deployment" || echo "No changes to commit"
git push --verbose
popd

24
.github/workflows/sidestore-shared.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: SideStore Shared
on:
workflow_call:
outputs:
short-commit:
value: ${{ jobs.shared.outputs.short-commit }}
jobs:
shared:
name: Shared Steps
strategy:
fail-fast: false
runs-on: 'macos-15'
steps:
- name: Set short commit hash
id: commit-id
run: |
# SHORT_COMMIT="${{ github.sha }}"
SHORT_COMMIT=${GITHUB_SHA:0:7}
echo "Short commit hash: $SHORT_COMMIT"
echo "SHORT_COMMIT=$SHORT_COMMIT" >> $GITHUB_OUTPUT
outputs:
short-commit: ${{ steps.commit-id.outputs.SHORT_COMMIT }}

View File

@@ -0,0 +1,204 @@
name: SideStore Tests Build
on:
workflow_call:
inputs:
release_tag:
type: string
short_commit:
type: string
secrets:
BUILD_LOG_ZIP_PASSWORD:
required: false
jobs:
tests-build:
name: Tests-Build SideStore - ${{ inputs.release_tag }}
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_BUILD == '1' }}
strategy:
fail-fast: false
matrix:
include:
- os: 'macos-15'
version: '16.2'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install dependencies - xcbeautify
run: |
brew install xcbeautify
shell: bash
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: '16.2'
# - name: (Tests-Build) Cache Build
# uses: irgaly/xcode-cache@v1.8.1
# with:
# key: xcode-cache-deriveddata-test-${{ github.ref_name }}-${{ github.sha }}
# # tests shouldn't restore cache unless it is same build
# # restore-keys: xcode-cache-deriveddata-test-${{ github.ref_name }}-
# swiftpm-cache-key: xcode-cache-sourcedata-test-${{ github.ref_name }}-${{ github.sha }}
# swiftpm-cache-restore-keys: |
# xcode-cache-sourcedata-test-${{ github.ref_name }}-
# delete-used-deriveddata-cache: true
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Exact match)
id: xcode-cache-restore
uses: actions/cache/restore@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Last Available)
id: xcode-cache-restore-recent
uses: actions/cache/restore@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-tests-${{ github.ref_name }}-
- name: (Tests-Build) Restore Pods from Cache (Exact match)
id: pods-restore
uses: actions/cache/restore@v3
with:
path: |
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-test-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
- name: (Tests-Build) Restore Pods from Cache (Last Available)
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
id: pods-restore-recent
uses: actions/cache/restore@v3
with:
path: |
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-test-${{ github.ref_name }}-
- name: (Tests-Build) Install CocoaPods
run: pod install
shell: bash
- name: (Tests-Build) Save Pods to Cache
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v3
with:
path: |
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-test-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
- name: Clean Derived Data (if required)
if: ${{ vars.PERFORM_CLEAN_TESTS_BUILD == '1' }}
run: |
rm -rf ~/Library/Developer/Xcode/DerivedData/
make clean
xcodebuild clean
shell: bash
- name: (Tests-Build) Clean previous build artifacts
run: |
make clean
mkdir -p build/logs
shell: bash
- name: (Tests-Build) List Files and derived data
shell: bash
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
echo ">>>>>>>>> Pods <<<<<<<<<<"
find Pods -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> SideStore <<<<<<<<<<"
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
echo ""
- name: Build SideStore Tests
# using 'tee' to intercept stdout and log for detailed build-log
shell: bash
run: |
NSUnbufferedIO=YES make -B build-tests 2>&1 | tee -a build/logs/tests-build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
- name: (Tests-Build) Save Xcode & SwiftPM Cache
id: cache-save
if: ${{ steps.xcode-cache-restore.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
- name: (Tests-Build) List Files and Build artifacts
if: always()
shell: bash
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
echo ">>>>>>>>> Build <<<<<<<<<<"
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
find ~/Library/Developer/Xcode/DerivedData -maxdepth 8 -exec ls -ld {} + | grep "Build/Products" >> tests-build-deriveddata.txt || true
echo ""
- uses: actions/upload-artifact@v4
if: always()
with:
name: tests-build-deriveddata-${{ inputs.short_commit }}.txt
path: tests-build-deriveddata.txt
- name: Encrypt tests-build-logs for upload
id: encrypt-test-log
if: always()
shell: bash
run: |
DEFAULT_BUILD_LOG_PASSWORD=12345
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
fi
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-tests-build-logs.zip * || popd
echo "::set-output name=encrypted::true"
- name: Upload encrypted-tests-build-logs.zip
id: attach-encrypted-test-log
if: always() && steps.encrypt-test-log.outputs.encrypted == 'true'
uses: actions/upload-artifact@v4
with:
name: encrypted-tests-build-logs-${{ inputs.short_commit }}.zip
path: encrypted-tests-build-logs.zip

View File

@@ -0,0 +1,235 @@
name: SideStore Tests Run
on:
workflow_call:
inputs:
release_tag:
type: string
short_commit:
type: string
secrets:
BUILD_LOG_ZIP_PASSWORD:
required: false
jobs:
tests-run:
name: Tests-Run SideStore - ${{ inputs.release_tag }}
if: ${{ vars.ENABLE_TESTS == '1' && vars.ENABLE_TESTS_RUN == '1' }}
strategy:
fail-fast: false
matrix:
include:
- os: 'macos-15'
version: '16.2'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Boot Simulator async(nohup) for testing
run: |
mkdir -p build/logs
nohup make -B boot-sim-async </dev/null >> build/logs/tests-run.log 2>&1 &
shell: bash
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: '16.2'
# - name: (Tests-Run) Cache Build
# uses: irgaly/xcode-cache@v1.8.1
# with:
# # This comes from
# key: xcode-cache-deriveddata-test-${{ github.ref_name }}-${{ github.sha }}
# swiftpm-cache-key: xcode-cache-sourcedata-test-${{ github.ref_name }}-${{ github.sha }}
- name: (Tests-Build) Restore Xcode & SwiftPM Cache (Exact match) [from tests-build job]
id: xcode-cache-restore
uses: actions/cache/restore@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-tests-${{ github.ref_name }}-${{ github.sha }}
- name: (Tests-Run) Restore Pods from Cache (Exact match)
id: pods-restore
uses: actions/cache/restore@v3
with:
path: |
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-test-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
- name: (Tests-Run) Restore Pods from Cache (Last Available)
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
id: pods-restore-recent
uses: actions/cache/restore@v3
with:
path: |
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-test-${{ github.ref_name }}-
- name: (Tests-Run) Install CocoaPods
run: pod install
shell: bash
- name: (Tests-Run) Save Pods to Cache
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v3
with:
path: |
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-test-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
- name: (Tests-Run) Clean previous build artifacts
run: |
make clean
mkdir -p build/logs
shell: bash
- name: (Tests-Run) List Files and derived data
shell: bash
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
echo ">>>>>>>>> Pods <<<<<<<<<<"
find Pods -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> SideStore <<<<<<<<<<"
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
find ~/Library/Developer/Xcode/DerivedData -maxdepth 8 -exec ls -ld {} + | grep "Build/Products" >> tests-run-deriveddata.txt || true
echo ""
- uses: actions/upload-artifact@v4
if: always()
with:
name: tests-run-deriveddata-${{ inputs.short_commit }}.txt
path: tests-run-deriveddata.txt
# we expect simulator to have been booted by now, so exit otherwise
- name: Simulator Boot Check
run: |
mkdir -p build/logs
make -B sim-boot-check | tee -a build/logs/tests-run.log
exit ${PIPESTATUS[0]}
shell: bash
- name: Start Recording UI tests (if DEBUG_RECORD_TESTS is set to 1)
if: ${{ vars.DEBUG_RECORD_TESTS == '1' }}
run: |
nohup xcrun simctl io booted recordVideo -f tests-recording.mp4 --codec h264 </dev/null > tests-recording.log 2>&1 &
RECORD_PID=$!
echo "RECORD_PID=$RECORD_PID" >> $GITHUB_ENV
shell: bash
- name: Run SideStore Tests
# using 'tee' to intercept stdout and log for detailed build-log
run: |
make run-tests 2>&1 | tee -a build/logs/tests-run.log && exit ${PIPESTATUS[0]}
# NSUnbufferedIO=YES make -B run-tests 2>&1 | tee build/logs/tests-run.log | xcpretty -r junit --output ./build/tests/test-results.xml && exit ${PIPESTATUS[0]}
shell: bash
- name: Stop Recording tests
if: ${{ always() && env.RECORD_PID != '' }}
run: |
kill -INT ${{ env.RECORD_PID }}
shell: bash
- name: (Tests-Run) List Files and Build artifacts
if: always()
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
echo ">>>>>>>>> Build <<<<<<<<<<"
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
echo ""
shell: bash
- name: Encrypt tests-run-logs for upload
id: encrypt-test-log
if: always()
run: |
DEFAULT_BUILD_LOG_PASSWORD=12345
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
fi
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-tests-run-logs.zip * || popd
echo "::set-output name=encrypted::true"
shell: bash
- name: Upload encrypted-tests-run-logs.zip
id: attach-encrypted-test-log
if: always() && steps.encrypt-test-log.outputs.encrypted == 'true'
uses: actions/upload-artifact@v4
with:
name: encrypted-tests-run-logs-${{ inputs.short_commit }}.zip
path: encrypted-tests-run-logs.zip
- name: Print tests-recording.log contents (if exists)
if: ${{ always() && env.RECORD_PID != '' }}
run: |
if [ -f tests-recording.log ]; then
echo "tests-recording.log found. Its contents:"
cat tests-recording.log
else
echo "tests-recording.log not found."
fi
shell: bash
- name: Check for tests-recording.mp4 presence
id: check-recording
if: ${{ always() && env.RECORD_PID != '' }}
run: |
if [ -f tests-recording.mp4 ]; then
echo "::set-output name=found::true"
echo "tests-recording.mp4 found."
else
echo "tests-recording.mp4 not found, skipping upload."
echo "::set-output name=found::false"
fi
shell: bash
- name: Upload tests-recording.mp4
id: upload-recording
if: ${{ always() && steps.check-recording.outputs.found == 'true' }}
uses: actions/upload-artifact@v4
with:
name: tests-recording-${{ inputs.short_commit }}.mp4
path: tests-recording.mp4
- name: Zip test-results
run: zip -r -9 ./test-results.zip ./build/tests
shell: bash
- name: Upload Test Artifacts
uses: actions/upload-artifact@v4
with:
name: test-results-${{ inputs.short_commit }}.zip
path: test-results.zip

View File

@@ -7,97 +7,277 @@ on:
jobs:
build:
name: Build and upload SideStore
name: Build SideStore - stable (on tag push)
strategy:
fail-fast: false
matrix:
include:
- os: 'macos-14'
version: '15.4'
- os: 'macos-15'
version: '16.2'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install dependencies
run: brew install ldid
- name: Echo Build.xcconfig
run: |
echo "cat Build.xcconfig"
cat Build.xcconfig
shell: bash
- name: Change version to tag
run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
# - name: Change MARKETING_VERSION to the pushed tag that triggered this build
# run: sed -e '/MARKETING_VERSION = .*/s/= .*/= ${{ github.ref_name }}/' -i '' Build.xcconfig
- name: Get version
- name: Echo Updated Build.xcconfig
run: |
cat Build.xcconfig
shell: bash
- name: Extract MARKETING_VERSION from Build.xcconfig
id: version
run: echo "version=$(grep MARKETING_VERSION Build.xcconfig | sed -e "s/MARKETING_VERSION = //g")" >> $GITHUB_OUTPUT
run: |
version=$(grep MARKETING_VERSION Build.xcconfig | sed -e 's/MARKETING_VERSION = //g')
echo "version=$version" >> $GITHUB_OUTPUT
echo "version=$version"
- name: Echo version
run: echo "${{ steps.version.outputs.version }}"
echo "MARKETING_VERSION=$version" >> $GITHUB_ENV
echo "MARKETING_VERSION=$version" >> $GITHUB_OUTPUT
echo "MARKETING_VERSION=$version"
shell: bash
- name: Fail the build if pushed tag and embedded MARKETING_VERSION in Build.xcconfig are mismatching
run: |
if [ "$MARKETING_VERSION" != "${{ github.ref_name }}" ]; then
echo 'Version mismatch: $tag != $marketing_version ... '
echo " expected-tag : $MARKETING_VERSION"
echo " pushed-tag : ${{ github.ref_name }}"
exit 1
fi
echo 'Version matches: $tag == $marketing_version ... '
echo " expected-tag : $MARKETING_VERSION"
echo " pushed-tag : ${{ github.ref_name }}"
shell: bash
- name: Install dependencies - ldid & xcbeautify
run: |
brew install ldid xcbeautify
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: ${{ matrix.version }}
- name: Cache Build
uses: irgaly/xcode-cache@v1
- name: (Build) Restore Xcode & SwiftPM Cache (Exact match)
id: xcode-cache-restore
uses: actions/cache/restore@v3
with:
key: xcode-cache-deriveddata-${{ github.sha }}
restore-keys: xcode-cache-deriveddata-
swiftpm-cache-key: xcode-cache-sourcedata-${{ github.sha }}
swiftpm-cache-restore-keys: |
xcode-cache-sourcedata-
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-stable-${{ github.sha }}
- name: Build SideStore
run: NSUnbufferedIO=YES make build | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
- name: (Build) Restore Xcode & SwiftPM Cache (Last Available)
id: xcode-cache-restore-recent
uses: actions/cache/restore@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-stable-
- name: (Build) Restore Pods from Cache (Exact match)
id: pods-restore
uses: actions/cache/restore@v3
with:
path: |
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-build-stable-${{ hashFiles('Podfile') }}
- name: (Build) Restore Pods from Cache (Last Available)
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
id: pods-restore-recent
uses: actions/cache/restore@v3
with:
path: |
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-build-stable-
- name: (Build) Install CocoaPods
run: pod install
shell: bash
- name: (Build) Save Pods to Cache
id: save-pods
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v3
with:
path: |
./Podfile.lock
./Pods/
./AltStore.xcworkspace/
key: pods-cache-build-stable-${{ hashFiles('Podfile') }}
- name: (Build) Clean previous build artifacts
run: |
make clean
mkdir -p build/logs
shell: bash
- name: (Build) List Files and derived data
if: always()
shell: bash
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
echo ">>>>>>>>> Pods <<<<<<<<<<"
find Pods -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> SideStore <<<<<<<<<<"
find SideStore -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Dependencies <<<<<<<<<<"
find Dependencies -maxdepth 2 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
echo ""
- name: Build SideStore.xcarchive
# using 'tee' to intercept stdout and log for detailed build-log
run: |
NSUnbufferedIO=YES make -B build 2>&1 | tee -a build/logs/build.log | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
shell: bash
- name: Fakesign app
run: make fakesign
run: make fakesign | tee -a build/logs/build.log
shell: bash
- name: Convert to IPA
run: make ipa
run: make ipa | tee -a build/logs/build.log
shell: bash
- name: Get current date
id: date
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
- name: Get current date in AltStore date form
id: date_altstore
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Upload to new stable release
uses: softprops/action-gh-release@v1
- name: (Build) Save Xcode & SwiftPM Cache
id: cache-save
if: ${{ steps.xcode-cache-restore.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
name: ${{ steps.version.outputs.version }}
tag_name: ${{ github.ref_name }}
draft: true
files: SideStore.ipa
body: |
<!-- NOTE: to reset SideSource cache, go to `https://apps.sidestore.io/reset-cache/nightly/<sidesource key>`. This is not included in the GitHub Action since it makes draft releases so they can be edited and have a changelog. -->
## Changelog
- TODO
## Build Info
Built at (UTC): `${{ steps.date.outputs.date }}`
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
Commit SHA: `${{ github.sha }}`
Version: `${{ steps.version.outputs.version }}`
path: |
~/Library/Developer/Xcode/DerivedData
~/Library/Caches/org.swift.swiftpm
key: xcode-cache-build-stable-${{ github.sha }}
- name: (Build) List Files and Build artifacts
run: |
echo ">>>>>>>>> Workdir <<<<<<<<<<"
ls -la .
echo ""
- name: Add version to IPA file name
run: mv SideStore.ipa SideStore-${{ steps.version.outputs.version }}.ipa
echo ">>>>>>>>> Build <<<<<<<<<<"
find build -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> SideStore <<<<<<<<<<"
find SideStore -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> SideStore.xcarchive <<<<<<<<<<"
find SideStore.xcarchive -maxdepth 3 -exec ls -ld {} + || true # List contents if directory exists
echo ""
echo ">>>>>>>>> Xcode-Derived-Data <<<<<<<<<<"
ls -la ~/Library/Developer/Xcode/DerivedData || true # List contents if directory exists
echo ""
shell: bash
- name: Encrypt build-logs for upload
id: encrypt-build-log
run: |
DEFAULT_BUILD_LOG_PASSWORD=12345
BUILD_LOG_ZIP_PASSWORD=${{ secrets.BUILD_LOG_ZIP_PASSWORD }}
BUILD_LOG_ZIP_PASSWORD=${BUILD_LOG_ZIP_PASSWORD:-$DEFAULT_BUILD_LOG_PASSWORD}
if [ "$BUILD_LOG_ZIP_PASSWORD" == "$DEFAULT_BUILD_LOG_PASSWORD" ]; then
echo "Warning: BUILD_LOG_ZIP_PASSWORD is not set. Defaulting to '${DEFAULT_BUILD_LOG_PASSWORD}'."
fi
pushd build/logs && zip -e -P "$BUILD_LOG_ZIP_PASSWORD" ../../encrypted-build-logs.zip * || popd
echo "::set-output name=encrypted::true"
shell: bash
- name: Upload encrypted-build-logs.zip
id: attach-encrypted-build-log
if: ${{ always() && steps.encrypt-build-log.outputs.encrypted == 'true' }}
uses: actions/upload-artifact@v4
with:
name: encrypted-build-logs-${{ steps.version.outputs.version }}.zip
path: encrypted-build-logs.zip
- name: Upload SideStore.ipa Artifact
uses: actions/upload-artifact@v4
with:
name: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore-${{ steps.version.outputs.version }}.ipa
path: SideStore.ipa
- name: Zip dSYMs
run: zip -r -9 ./SideStore.dSYMs.zip ./SideStore.xcarchive/dSYMs
shell: bash
- name: Upload *.dSYM Artifact
uses: actions/upload-artifact@v4
with:
name: SideStore-${{ steps.version.outputs.version }}-dSYM
path: ./*.dSYM/
name: SideStore-${{ steps.version.outputs.version }}-dSYMs.zip
path: SideStore.dSYMs.zip
- name: Get current date
id: date
run: echo "date=$(date -u +'%c')" >> $GITHUB_OUTPUT
shell: bash
- name: Get current date in AltStore date form
id: date_altstore
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
shell: bash
- name: Upload to releases
uses: IsaacShelton/update-existing-release@v1.3.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
draft: true
release: ${{ github.ref_name }} # name
tag: ${{ github.ref_name }}
# stick with what the user pushed, do not use latest commit or anything,
# ex: if we want to go back to previous release due to hot issue, dev can create a new tag pointing to that older working tag/commit so as to keep it as an update (to revert major issue)
# in this case we do not want the tag to be auto-updated to latest
updateTag: false
prerelease: false
files: >
SideStore.ipa
SideStore.dSYMs.zip
encrypted-build-logs.zip
body: |
<!-- NOTE: to reset SideSource cache, go to `https://apps.sidestore.io/reset-cache/nightly/<sidesource key>`. This is not included in the GitHub Action since it makes draft releases so they can be edited and have a changelog. -->
## Changelog
- TODO
## Build Info
Built at (UTC): `${{ steps.date.outputs.date }}`
Built at (UTC date): `${{ steps.date_altstore.outputs.date }}`
Commit SHA: `${{ github.sha }}`
Version: `${{ steps.version.outputs.version }}`

View File

@@ -87,6 +87,9 @@
A8AD35592D31BF2C003A28B4 /* PageInfoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8AD35582D31BF29003A28B4 /* PageInfoManager.swift */; };
A8B516E32D2666CA0047047C /* CoreDataHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B516E22D2666CA0047047C /* CoreDataHelper.swift */; };
A8B516E62D2668170047047C /* DateTimeUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B516E52D2668020047047C /* DateTimeUtil.swift */; };
A8B645FC2D70C10300125819 /* CollapsingMarkdownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B645FB2D70C10300125819 /* CollapsingMarkdownView.swift */; };
A8B645FF2D70C1AD00125819 /* MarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = A8B645FE2D70C1AD00125819 /* MarkdownKit */; };
A8B646012D70C23E00125819 /* MarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = A8B646002D70C23E00125819 /* MarkdownKit */; };
A8BB34E52D04EC8E000A8B4D /* minimuxer-helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A52D04DA1900F0F0F3 /* minimuxer-helpers.swift */; };
A8C38C242D206A3A00E83DBD /* ConsoleLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C1D2D206A3A00E83DBD /* ConsoleLogger.swift */; };
A8C38C262D206A3A00E83DBD /* ConsoleLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C1E2D206A3A00E83DBD /* ConsoleLog.swift */; };
@@ -678,6 +681,7 @@
A8696EE32D34512C00E96389 /* RemoveAppExtensionsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveAppExtensionsOperation.swift; sourceTree = "<group>"; };
A881E7C62D6EF58C00954AD2 /* AltStore11ToAltStore17.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore11ToAltStore17.xcmappingmodel; sourceTree = "<group>"; };
A881E7CA2D6EF5AB00954AD2 /* StoreApp11To17MigrationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreApp11To17MigrationPolicy.swift; sourceTree = "<group>"; };
A881E8562D6FBBAF00954AD2 /* DataStructureTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DataStructureTests.xcconfig; sourceTree = "<group>"; };
A88B8C482D35AD3200F53F9D /* OperationsLoggingContolView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationsLoggingContolView.swift; sourceTree = "<group>"; };
A88B8C542D35F1EC00F53F9D /* OperationsLoggingControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationsLoggingControl.swift; sourceTree = "<group>"; };
A8945AA52D059B6100D86CBE /* Roxas.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Roxas.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -685,6 +689,7 @@
A8AD35582D31BF29003A28B4 /* PageInfoManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageInfoManager.swift; sourceTree = "<group>"; };
A8B516E22D2666CA0047047C /* CoreDataHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataHelper.swift; sourceTree = "<group>"; };
A8B516E52D2668020047047C /* DateTimeUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeUtil.swift; sourceTree = "<group>"; };
A8B645FB2D70C10300125819 /* CollapsingMarkdownView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsingMarkdownView.swift; sourceTree = "<group>"; };
A8C38C1D2D206A3A00E83DBD /* ConsoleLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLogger.swift; sourceTree = "<group>"; };
A8C38C1E2D206A3A00E83DBD /* ConsoleLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLog.swift; sourceTree = "<group>"; };
A8C38C282D206AC100E83DBD /* OutputStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputStream.swift; sourceTree = "<group>"; };
@@ -1136,6 +1141,7 @@
files = (
A8C6D5172D1EE95B00DF01F1 /* OpenSSL.xcframework in Frameworks */,
A8A543302D04F14400D72399 /* libfragmentzip.a in Frameworks */,
A8B646012D70C23E00125819 /* MarkdownKit in Frameworks */,
A805C3CD2D0C316A00E76BDD /* Pods_SideStore.framework in Frameworks */,
A8F838942D048ECE00ED425D /* libimobiledevice.a in Frameworks */,
A8F838922D048E8F00ED425D /* libEmotionalDamage.a in Frameworks */,
@@ -1143,6 +1149,7 @@
D533E8B72727841800A9B5DD /* libAppleArchive.tbd in Frameworks */,
D533E8BE2727BBF800A9B5DD /* libcurl.a in Frameworks */,
BF1614F1250822F100767AEA /* Roxas.framework in Frameworks */,
A8B645FF2D70C1AD00125819 /* MarkdownKit in Frameworks */,
BF66EE852501AE50007EE018 /* AltStoreCore.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1269,6 +1276,7 @@
A85ACB8E2D1F31C400AA3DE7 /* AltBackup.xcconfig */,
A85ACB932D1F31C400AA3DE7 /* AltWidgetExtension.xcconfig */,
A8E2DB2C2D684D39009E5D31 /* UITests.xcconfig */,
A881E8562D6FBBAF00954AD2 /* DataStructureTests.xcconfig */,
);
path = xcconfigs;
sourceTree = "<group>";
@@ -1344,6 +1352,22 @@
path = database;
sourceTree = "<group>";
};
A8B645F82D70C0DD00125819 /* Views */ = {
isa = PBXGroup;
children = (
A8B645FA2D70C0F600125819 /* UIKit */,
);
path = Views;
sourceTree = "<group>";
};
A8B645FA2D70C0F600125819 /* UIKit */ = {
isa = PBXGroup;
children = (
A8B645FB2D70C10300125819 /* CollapsingMarkdownView.swift */,
);
path = UIKit;
sourceTree = "<group>";
};
A8C38C1C2D2068D100E83DBD /* Utils */ = {
isa = PBXGroup;
children = (
@@ -1425,6 +1449,7 @@
A8F66C072D04C025009689E6 /* SideStore */ = {
isa = PBXGroup;
children = (
A8B645F82D70C0DD00125819 /* Views */,
A8E2DB352D6850A9009E5D31 /* Tests */,
A8F66C5C2D04D433009689E6 /* minimuxer */,
A8F66C602D04D464009689E6 /* minimuxer.xcodeproj */,
@@ -2680,6 +2705,7 @@
D58D5F2C26DFE68E00E55E38 /* XCRemoteSwiftPackageReference "LaunchAtLogin" */,
D5FB7A2C2AA2859400EF863D /* XCRemoteSwiftPackageReference "swift-argument-parser" */,
A82067C22D03E0DE00645C0D /* XCRemoteSwiftPackageReference "SemanticVersion" */,
A8B645FD2D70C1AD00125819 /* XCRemoteSwiftPackageReference "MarkdownKit" */,
);
productRefGroup = BFD2476B2284B9A500981D42 /* Products */;
projectDirPath = "";
@@ -3290,6 +3316,7 @@
D57FE84428C7DB7100216002 /* ErrorLogViewController.swift in Sources */,
A88B8C552D35F1EC00F53F9D /* OperationsLoggingControl.swift in Sources */,
D50107EC2ADF2E1A0069F2A1 /* AddSourceTextFieldCell.swift in Sources */,
A8B645FC2D70C10300125819 /* CollapsingMarkdownView.swift in Sources */,
D5151BD92A8FF64300C96F28 /* RefreshAllAppsIntent.swift in Sources */,
BFBE0007250AD0E70080826E /* ViewAppIntentHandler.swift in Sources */,
BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */,
@@ -3532,6 +3559,7 @@
};
A81A8CCA2D68BA610086C96F /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = A881E8562D6FBBAF00954AD2 /* DataStructureTests.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
@@ -3546,7 +3574,7 @@
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.SideStore.SideStore.DataStructuresTests;
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = NO;
@@ -3557,6 +3585,7 @@
};
A81A8CCB2D68BA610086C96F /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = A881E8562D6FBBAF00954AD2 /* DataStructureTests.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
@@ -3573,7 +3602,7 @@
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.SideStore.SideStore.DataStructuresTests;
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_COMPILATION_MODE = wholemodule;
@@ -3600,7 +3629,7 @@
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "--PRODUCT-BUNDLE-IDENTIFIER-.UITests";
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = NO;
@@ -3618,6 +3647,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -3626,7 +3656,7 @@
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "--PRODUCT-BUNDLE-IDENTIFIER-.UITests";
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = NO;
@@ -4254,6 +4284,14 @@
minimumVersion = 0.4.0;
};
};
A8B645FD2D70C1AD00125819 /* XCRemoteSwiftPackageReference "MarkdownKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/bmoliveira/MarkdownKit.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.7.1;
};
};
D58D5F2C26DFE68E00E55E38 /* XCRemoteSwiftPackageReference "LaunchAtLogin" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin.git";
@@ -4278,6 +4316,16 @@
package = A82067C22D03E0DE00645C0D /* XCRemoteSwiftPackageReference "SemanticVersion" */;
productName = SemanticVersion;
};
A8B645FE2D70C1AD00125819 /* MarkdownKit */ = {
isa = XCSwiftPackageProductDependency;
package = A8B645FD2D70C1AD00125819 /* XCRemoteSwiftPackageReference "MarkdownKit" */;
productName = MarkdownKit;
};
A8B646002D70C23E00125819 /* MarkdownKit */ = {
isa = XCSwiftPackageProductDependency;
package = A8B645FD2D70C1AD00125819 /* XCRemoteSwiftPackageReference "MarkdownKit" */;
productName = MarkdownKit;
};
A8C6D50B2D1EE87600DF01F1 /* AltSign-Static */ = {
isa = XCSwiftPackageProductDependency;
productName = "AltSign-Static";

View File

@@ -43,8 +43,10 @@ final class AppContentViewController: UITableViewController
}()
@IBOutlet private var subtitleLabel: UILabel!
@IBOutlet private var descriptionTextView: CollapsingTextView!
@IBOutlet private var versionDescriptionTextView: CollapsingTextView!
// @IBOutlet private var descriptionTextView: CollapsingTextView!
@IBOutlet private var descriptionTextView: CollapsingMarkdownView!
// @IBOutlet private var versionDescriptionTextView: CollapsingTextView!
@IBOutlet private var versionDescriptionTextView: CollapsingMarkdownView!
@IBOutlet private var versionLabel: UILabel!
@IBOutlet private var versionDateLabel: UILabel!
@IBOutlet private var sizeLabel: UILabel!
@@ -55,35 +57,32 @@ final class AppContentViewController: UITableViewController
@IBOutlet private(set) var appDetailCollectionViewController: AppDetailCollectionViewController!
@IBOutlet private var appDetailCollectionViewHeightConstraint: NSLayoutConstraint!
override func viewDidLoad()
{
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.contentInset.bottom = 20
self.subtitleLabel.text = self.app.subtitle
self.descriptionTextView.text = self.app.localizedDescription
let desc = self.app.localizedDescription
self.descriptionTextView.text = desc
if let version = self.app.latestAvailableVersion
{
self.versionDescriptionTextView.text = version.localizedDescription
if let version = self.app.latestAvailableVersion {
self.versionDescriptionTextView.text = version.localizedDescription ?? "nil"
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.localizedVersion)
self.versionDateLabel.text = Date().relativeDateString(since: version.date)
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: version.size)
}
else
{
self.versionDescriptionTextView.text = nil
self.sizeLabel.text = ByteCountFormatter.string(fromByteCount: version.size, countStyle: .file)
} else {
self.versionDescriptionTextView.text = "nil"
self.versionLabel.text = nil
self.versionDateLabel.text = nil
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: 0)
self.sizeLabel.text = ByteCountFormatter.string(fromByteCount: 0, countStyle: .file)
}
self.descriptionTextView.maximumNumberOfLines = 5
self.descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
self.versionDescriptionTextView.maximumNumberOfLines = 5
self.versionDescriptionTextView.maximumNumberOfLines = 3
self.versionDescriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
self.descriptionTextView.toggleButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
self.versionDescriptionTextView.toggleButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
}
override func viewDidLayoutSubviews()
@@ -162,8 +161,12 @@ private extension AppContentViewController
switch sender
{
case self.descriptionTextView.moreButton: indexPath = IndexPath(row: Row.description.rawValue, section: 0)
case self.versionDescriptionTextView.moreButton: indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0)
case self.descriptionTextView.toggleButton:
indexPath = IndexPath(row: Row.description.rawValue, section: 0)
case self.versionDescriptionTextView.toggleButton:
indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0)
default: return
}

View File

@@ -287,7 +287,7 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="98"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Hello Me" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="Pyt-8D-BZA" customClass="CollapsingTextView" customModule="SideStore" customModuleProvider="target">
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Hello Me" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="Pyt-8D-BZA" customClass="CollapsingMarkdownView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="20" y="20" width="335" height="34"/>
<color key="backgroundColor" name="Background"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
@@ -353,7 +353,7 @@
</stackView>
</subviews>
</stackView>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="wQF-WY-Gdz" customClass="CollapsingTextView" customModule="SideStore" customModuleProvider="target">
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="wQF-WY-Gdz" customClass="CollapsingMarkdownView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="20" y="59.5" width="335" height="34"/>
<color key="backgroundColor" name="Background"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>

View File

@@ -13,21 +13,18 @@ final class CollapsingTextView: UITextView
var isCollapsed = true {
didSet {
guard self.isCollapsed != oldValue else { return }
self.shouldResetLayout = true
self.setNeedsLayout()
}
}
var maximumNumberOfLines = 2 {
didSet {
self.shouldResetLayout = true
self.setNeedsLayout()
}
}
var lineSpacing: Double = 2 {
didSet {
self.shouldResetLayout = true
if #available(iOS 16, *)
{
@@ -42,7 +39,6 @@ final class CollapsingTextView: UITextView
override var text: String! {
didSet {
self.shouldResetLayout = true
guard #available(iOS 16, *) else { return }
self.updateText()
@@ -51,9 +47,6 @@ final class CollapsingTextView: UITextView
let moreButton = UIButton(type: .system)
private var shouldResetLayout: Bool = false
private var previousSize: CGSize?
override init(frame: CGRect, textContainer: NSTextContainer?)
{
super.init(frame: frame, textContainer: textContainer)
@@ -115,45 +108,39 @@ final class CollapsingTextView: UITextView
height: font.lineHeight)
self.moreButton.frame = moreButtonFrame
if self.shouldResetLayout || self.previousSize != self.bounds.size
if self.isCollapsed
{
if self.isCollapsed
let boundingSize = self.attributedText.boundingRect(with: CGSize(width: self.textContainer.size.width, height: .infinity), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
let maximumCollapsedHeight = font.lineHeight * Double(self.maximumNumberOfLines) + self.lineSpacing * Double(self.maximumNumberOfLines - 1)
if boundingSize.height.rounded() > maximumCollapsedHeight.rounded()
{
let boundingSize = self.attributedText.boundingRect(with: CGSize(width: self.textContainer.size.width, height: .infinity), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
let maximumCollapsedHeight = font.lineHeight * Double(self.maximumNumberOfLines) + self.lineSpacing * Double(self.maximumNumberOfLines - 1)
self.textContainer.maximumNumberOfLines = self.maximumNumberOfLines
if boundingSize.height.rounded() > maximumCollapsedHeight.rounded()
{
self.textContainer.maximumNumberOfLines = self.maximumNumberOfLines
var exclusionFrame = moreButtonFrame
exclusionFrame.origin.y += self.moreButton.bounds.midY
exclusionFrame.size.width = self.bounds.width // Extra wide to make sure it wraps to next line.
self.textContainer.exclusionPaths = [UIBezierPath(rect: exclusionFrame)]
self.moreButton.isHidden = false
}
else
{
self.textContainer.maximumNumberOfLines = 0 // Fixes last line having slightly smaller line spacing.
self.textContainer.exclusionPaths = []
self.moreButton.isHidden = true
}
var exclusionFrame = moreButtonFrame
exclusionFrame.origin.y += self.moreButton.bounds.midY
exclusionFrame.size.width = self.bounds.width // Extra wide to make sure it wraps to next line.
self.textContainer.exclusionPaths = [UIBezierPath(rect: exclusionFrame)]
self.moreButton.isHidden = false
}
else
{
self.textContainer.maximumNumberOfLines = 0
self.textContainer.maximumNumberOfLines = 0 // Fixes last line having slightly smaller line spacing.
self.textContainer.exclusionPaths = []
self.moreButton.isHidden = true
}
}
else
{
self.textContainer.maximumNumberOfLines = 0
self.textContainer.exclusionPaths = []
self.invalidateIntrinsicContentSize()
self.moreButton.isHidden = true
}
self.shouldResetLayout = false
self.previousSize = self.bounds.size
self.invalidateIntrinsicContentSize()
}
}

View File

@@ -242,7 +242,6 @@
<key>public.filename-extension</key>
<array>
<string>mobiledevicepairing</string>
<string>mobiledevicepair</string>
</array>
</dict>
</dict>

View File

@@ -235,7 +235,8 @@ private extension MyAppsViewController
cell.layoutMargins.right = self.view.layoutMargins.right
cell.tintColor = app.tintColor ?? .altPrimary
cell.versionDescriptionTextView.text = latestSupportedVersion.localizedDescription
cell.versionDescriptionTextView.maximumNumberOfLines = 2
cell.versionDescriptionTextView.text = latestSupportedVersion.localizedDescription ?? "nil"
cell.bannerView.iconImageView.image = nil
cell.bannerView.iconImageView.isIndicatingActivity = true
@@ -281,12 +282,9 @@ private extension MyAppsViewController
cell.mode = .collapsed
}
cell.versionDescriptionTextView.moreButton.addTarget(self, action: #selector(MyAppsViewController.toggleUpdateCellMode(_:)), for: .primaryActionTriggered)
cell.versionDescriptionTextView.toggleButton.addTarget(self, action: #selector(MyAppsViewController.toggleUpdateCellMode(_:)), for: .primaryActionTriggered)
cell.setNeedsLayout()
// Below lines are necessary to avoid "more" button layout issues.
cell.versionDescriptionTextView.setNeedsLayout()
cell.layoutIfNeeded()
}
dataSource.prefetchHandler = { (installedApp, indexPath, completionHandler) in
@@ -724,22 +722,29 @@ private extension MyAppsViewController
let cell = self.collectionView.cellForItem(at: indexPath) as? UpdateCollectionViewCell
// Toggle the state
if self.expandedAppUpdates.contains(installedApp.bundleIdentifier)
{
self.expandedAppUpdates.remove(installedApp.bundleIdentifier)
// Set collapsed mode on the cell
cell?.mode = .collapsed
}
else
{
self.expandedAppUpdates.insert(installedApp.bundleIdentifier)
// Set expanded mode on the cell
cell?.mode = .expanded
}
// Clear cached size so it's recalculated
self.cachedUpdateSizes[installedApp.bundleIdentifier] = nil
self.collectionView.performBatchUpdates({
self.collectionView.collectionViewLayout.invalidateLayout()
}, completion: nil)
// Animate the change smoothly with a duration
UIView.animate(withDuration: 0.25) {
self.collectionView.performBatchUpdates({
self.collectionView.collectionViewLayout.invalidateLayout()
}, completion: nil)
}
}
@IBAction func refreshApp(_ sender: UIButton)

View File

@@ -21,12 +21,19 @@ extension UpdateCollectionViewCell
{
var mode: Mode = .expanded {
didSet {
self.update()
switch self.mode {
case .collapsed:
self.versionDescriptionTextView.isCollapsed = true
case .expanded:
self.versionDescriptionTextView.isCollapsed = false
}
self.setNeedsLayout()
}
}
@IBOutlet var bannerView: AppBannerView!
@IBOutlet var versionDescriptionTextView: CollapsingTextView!
// @IBOutlet var versionDescriptionTextView: CollapsingTextView!
@IBOutlet var versionDescriptionTextView: CollapsingMarkdownView!
@IBOutlet private var blurView: UIVisualEffectView!
@@ -85,16 +92,16 @@ extension UpdateCollectionViewCell
}
}
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize
{
// Ensure cell is laid out so it will report correct size.
self.versionDescriptionTextView.setNeedsLayout()
self.versionDescriptionTextView.layoutIfNeeded()
let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority)
return size
}
// override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize
// {
// // Ensure cell is laid out so it will report correct size.
// self.versionDescriptionTextView.setNeedsLayout()
// self.versionDescriptionTextView.layoutIfNeeded()
//
// let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority)
//
// return size
// }
}
private extension UpdateCollectionViewCell

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@@ -11,7 +11,7 @@
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="UpdateCell" id="Kqf-Pv-ca3" customClass="UpdateCollectionViewCell" customModule="AltStore" customModuleProvider="target">
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="UpdateCell" id="Kqf-Pv-ca3" customClass="UpdateCollectionViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="125"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
@@ -30,7 +30,7 @@
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="uYl-PH-DuP">
<rect key="frame" x="0.0" y="0.0" width="343" height="125"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Nop-pL-Icx" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Nop-pL-Icx" customClass="AppBannerView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="343" height="50"/>
<constraints>
<constraint firstAttribute="height" constant="88" id="EPP-7O-1Ad"/>
@@ -39,7 +39,7 @@
<stackView opaque="NO" contentMode="scaleToFill" alignment="firstBaseline" translatesAutoresizingMaskIntoConstraints="NO" id="RSR-5W-7tt" userLabel="Release Notes">
<rect key="frame" x="0.0" y="50" width="343" height="75"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Version Notes" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rNs-2O-k3V" customClass="CollapsingTextView" customModule="AltStore" customModuleProvider="target">
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Version Notes" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rNs-2O-k3V" customClass="CollapsingMarkdownView" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="15" y="0.0" width="313" height="26"/>
<color key="textColor" systemColor="secondaryLabelColor"/>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
@@ -91,7 +91,7 @@
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<systemColor name="secondaryLabelColor">
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>

View File

@@ -360,13 +360,6 @@ extension FetchProvisioningProfilesOperation
}
}
class FetchProvisioningProfilesRefreshOperation: FetchProvisioningProfilesOperation, @unchecked Sendable {
override init(context: AppOperationContext)
{
super.init(context: context)
}
}
class FetchProvisioningProfilesInstallOperation: FetchProvisioningProfilesOperation, @unchecked Sendable{
override init(context: AppOperationContext)
{
@@ -613,3 +606,14 @@ class FetchProvisioningProfilesInstallOperation: FetchProvisioningProfilesOperat
}
}
}
// <TEST> : users were reporting that refresh (though seemed like it refreshed the app becomes no longer available)
// possibly, this is caused since refesh was not updating appFeatures and AppGroups in the new profile? not sure.
// for now we are reverting by keeping same operation that happens during fetch in install path to see if it fixes issue #893
// class FetchProvisioningProfilesRefreshOperation: FetchProvisioningProfilesOperation, @unchecked Sendable {
class FetchProvisioningProfilesRefreshOperation: FetchProvisioningProfilesInstallOperation, @unchecked Sendable {
override init(context: AppOperationContext)
{
super.init(context: context)
}
}

View File

@@ -141,7 +141,7 @@ public class InstalledApp: BaseEntity, InstalledAppProtocol
let latestVer = SemanticVersion("\(latestSemVer!.major).\(latestSemVer!.minor).\(latestSemVer!.patch)")
// Compare by major.minor.patch
if latestVer! > latestVer! {
if latestVer! > currentVer! {
return true
}

View File

@@ -4,7 +4,7 @@
DEVELOPMENT_TEAM = XYZ0123456
// Set this for dev-local xcode builds
MARKETING_VERSION_SUFFIX = -local
//MARKETING_VERSION_SUFFIX = -local
// Prefix of unique bundle IDs registered to you in Apple Developer Portal.
// You need to register:

View File

@@ -213,6 +213,7 @@ build-tests:
@echo "Performing a build-for-testing..."
@xcodebuild build-for-testing \
-enableCodeCoverage YES \
-destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.2' \
$(COMMON_BUILD_SETTINGS)
run-tests:

View File

@@ -0,0 +1,301 @@
//
// CollapsingMarkdownView.swift
// AltStore
//
// Created by Magesh K on 27/02/25.
// Copyright © 2025 SideStore. All rights reserved.
//
import UIKit
import MarkdownKit
struct MarkdownManager
{
struct Fonts{
static let body: UIFont = .systemFont(ofSize: UIFont.systemFontSize)
// static let body: UIFont = .systemFont(ofSize: UIFont.labelFontSize)
static let header: UIFont = .boldSystemFont(ofSize: 14)
static let list: UIFont = .systemFont(ofSize: 14)
static let bold: UIFont = .boldSystemFont(ofSize: 14)
static let italic: UIFont = .italicSystemFont(ofSize: 14)
static let quote: UIFont = .italicSystemFont(ofSize: 14)
}
struct Color{
static let header = UIColor { traitCollection in
traitCollection.userInterfaceStyle == .dark ? UIColor.white : UIColor.black
}
static let bold = UIColor { traitCollection in
traitCollection.userInterfaceStyle == .dark ? UIColor.lightText : UIColor.darkText
}
}
static var enabledElements: MarkdownParser.EnabledElements {
[
.header,
.list,
.quote,
.code,
.link,
.bold,
.italic,
]
}
var markdownParser: MarkdownParser {
MarkdownParser(
font: Self.Fonts.body,
color: Self.Color.bold
)
}
}
final class CollapsingMarkdownView: UIView {
/// Called when the collapse state toggles.
var didToggleCollapse: (() -> Void)?
// MARK: - Properties
var isCollapsed = true {
didSet {
guard self.isCollapsed != oldValue else { return }
self.updateCollapsedState()
}
}
var maximumNumberOfLines = 3 {
didSet {
self.checkIfNeedsCollapsing()
self.updateCollapsedState()
self.setNeedsLayout()
}
}
var text: String = "" {
didSet {
self.updateMarkdownContent()
self.setNeedsLayout()
}
}
var lineSpacing: Double = 2 {
didSet {
self.setNeedsLayout()
}
}
let toggleButton = UIButton(type: .system)
private let textView = UITextView()
private let markdownParser = MarkdownManager().markdownParser
private var previousSize: CGSize?
private var actualLineCount: Int = 0
private var needsCollapsing = false
// MARK: - Initialization
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
initialize()
}
override func awakeFromNib() {
super.awakeFromNib()
initialize()
}
private func checkIfNeedsCollapsing() {
guard bounds.width > 0, let font = textView.font, font.lineHeight > 0 else {
needsCollapsing = false
return
}
// Calculate the number of lines in the text
let textSize = textView.sizeThatFits(CGSize(width: bounds.width, height: .greatestFiniteMagnitude))
let lineHeight = font.lineHeight
// Safely calculate actual line count
actualLineCount = max(1, Int(ceil(textSize.height / lineHeight)))
// Only needs collapsing if actual lines exceed the maximum
needsCollapsing = actualLineCount > maximumNumberOfLines
// Update button visibility
toggleButton.isHidden = !needsCollapsing
}
private func updateCollapsedState() {
// Disable animations for this update
UIView.performWithoutAnimation {
// Update the button title
let title = isCollapsed ? NSLocalizedString("More", comment: "") : NSLocalizedString("Less", comment: "")
toggleButton.setTitle(title, for: .normal)
// Set max lines based on collapsed state
if isCollapsed && needsCollapsing {
textView.textContainer.maximumNumberOfLines = maximumNumberOfLines
} else {
textView.textContainer.maximumNumberOfLines = 0
}
// Button is only visible if content needs collapsing
toggleButton.isHidden = !needsCollapsing
// Force layout updates
textView.layoutIfNeeded()
self.layoutIfNeeded()
self.invalidateIntrinsicContentSize()
}
}
private func initialize() {
// Configure text view
textView.isEditable = false
textView.isScrollEnabled = false
textView.textContainerInset = .zero
textView.textContainer.lineFragmentPadding = 0
textView.textContainer.lineBreakMode = .byTruncatingTail
textView.backgroundColor = .clear
// Make textView selectable to enable link interactions
textView.isSelectable = true
textView.delegate = self
// Important: This prevents selection handles from appearing
textView.dataDetectorTypes = .link
// Configure markdown parser
configureMarkdownParser()
// Add subviews
addSubview(textView)
// Configure toggle button
toggleButton.addTarget(self, action: #selector(toggleCollapsed(_:)), for: .primaryActionTriggered)
addSubview(toggleButton)
setNeedsLayout()
}
private func configureMarkdownParser() {
// Configure markdown parser with desired settings
markdownParser.enabledElements = MarkdownManager.enabledElements
// You can also customize the styling if needed
markdownParser.header.font = MarkdownManager.Fonts.header
markdownParser.list.font = MarkdownManager.Fonts.list
markdownParser.bold.font = MarkdownManager.Fonts.bold
markdownParser.italic.font = MarkdownManager.Fonts.italic
markdownParser.quote.font = MarkdownManager.Fonts.quote
markdownParser.header.color = MarkdownManager.Color.header
markdownParser.bold.color = MarkdownManager.Color.bold
markdownParser.list.color = MarkdownManager.Color.bold
}
// MARK: - Layout
override func layoutSubviews() {
super.layoutSubviews()
UIView.performWithoutAnimation {
// Calculate button height (for spacing)
let buttonHeight = toggleButton.sizeThatFits(CGSize(width: 1000, height: 1000)).height
// Set textView frame to leave space for button
textView.frame = CGRect(
x: 0,
y: 0,
width: bounds.width,
height: bounds.height - buttonHeight
)
// Check if layout changed
if previousSize?.width != bounds.width {
checkIfNeedsCollapsing()
updateCollapsedState()
previousSize = bounds.size
}
// Position toggle button at bottom right
let buttonSize = toggleButton.sizeThatFits(CGSize(width: 1000, height: 1000))
toggleButton.frame = CGRect(
x: bounds.width - buttonSize.width,
y: textView.frame.maxY,
width: buttonSize.width,
height: buttonHeight
)
}
}
@objc private func toggleCollapsed(_ sender: UIButton) {
isCollapsed.toggle()
didToggleCollapse?()
}
override var intrinsicContentSize: CGSize {
guard bounds.width > 0, let font = textView.font, font.lineHeight > 0 else {
return CGSize(width: UIView.noIntrinsicMetric, height: 0)
}
let lineHeight = font.lineHeight
let buttonHeight = toggleButton.sizeThatFits(CGSize(width: 1000, height: 1000)).height
// Always add button height to reserve space for it
if isCollapsed && needsCollapsing {
// When collapsed and needs collapsing, use maximumNumberOfLines
let collapsedHeight = lineHeight * CGFloat(maximumNumberOfLines) +
lineSpacing * CGFloat(max(0, maximumNumberOfLines - 1))
return CGSize(width: UIView.noIntrinsicMetric, height: collapsedHeight + buttonHeight)
} else if !needsCollapsing {
// Text is shorter than max lines - use actual text height
let textSize = textView.sizeThatFits(CGSize(width: bounds.width, height: .greatestFiniteMagnitude))
return CGSize(width: UIView.noIntrinsicMetric, height: textSize.height + buttonHeight)
} else {
// When expanded and needs collapsing, use full text height plus button
let textSize = textView.sizeThatFits(CGSize(width: bounds.width, height: .greatestFiniteMagnitude))
return CGSize(width: UIView.noIntrinsicMetric, height: textSize.height + buttonHeight)
}
}
// MARK: - Markdown Processing
private func updateMarkdownContent() {
let attributedString = markdownParser.parse(text)
// Apply line spacing
let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = lineSpacing
mutableAttributedString.addAttribute(
.paragraphStyle,
value: paragraphStyle,
range: NSRange(location: 0, length: mutableAttributedString.length)
)
textView.attributedText = mutableAttributedString
// Check if content needs collapsing after setting text
checkIfNeedsCollapsing()
updateCollapsedState()
}
}
extension CollapsingMarkdownView: UITextViewDelegate {
// This enables tapping on links while preventing text selection
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
// Open the URL using UIApplication
UIApplication.shared.open(URL)
return false // Return false to prevent the default behavior
}
// This prevents text selection
func textViewDidChangeSelection(_ textView: UITextView) {
textView.selectedTextRange = nil
}
}

View File

@@ -5,7 +5,7 @@ import os
import re
IGNORED_AUTHORS = [
"=", # probably someone used an equalTo ?! # anyway we are ignoring it!
]
TAG_MARKER = "###"
@@ -82,7 +82,7 @@ def format_commit_message(msg):
msg_clean = msg_clean[1:].strip() # remove leading '-' and spaces
return f"- {msg_clean}"
def generate_release_notes(last_successful, tag, branch):
# def generate_release_notes(last_successful, tag, branch):
"""Generate release notes for the given tag."""
current_commit = get_head_commit()
messages = get_commit_messages(last_successful, current_commit)
@@ -117,6 +117,50 @@ def generate_release_notes(last_successful, tag, branch):
return new_section
def generate_release_notes(last_successful, tag, branch):
"""Generate release notes for the given tag."""
current_commit = get_head_commit()
try:
# Try to get commit messages using the provided last_successful commit
messages = get_commit_messages(last_successful, current_commit)
except subprocess.CalledProcessError:
# If the range is invalid (e.g. force push made last_successful obsolete),
# fall back to using the last 10 commits in the current branch.
print("\nInvalid revision range error, using last 10 commits as fallback.\n")
fallback_commit = run_command("git rev-parse HEAD~5")
messages = get_commit_messages(fallback_commit, current_commit)
last_successful = fallback_commit
# Start with the tag header
new_section = f"{TAG_MARKER} {tag}\n"
# What's Changed section (always present)
new_section += f"{HEADER_MARKER} What's Changed\n"
if not messages or last_successful == current_commit:
new_section += "- Nothing...\n"
else:
for msg in messages:
new_section += f"{format_commit_message(msg)}\n"
# New Contributors section (only if there are new contributors)
all_previous_authors = get_authors_in_range(f"{branch}")
recent_authors = get_authors_in_range(f"{last_successful}..{current_commit}")
new_contributors = recent_authors - all_previous_authors
if new_contributors:
new_section += f"\n{HEADER_MARKER} New Contributors\n"
for author in sorted(new_contributors):
new_section += f"- {format_contributor(author)} made their first contribution\n"
# Full Changelog section (only if there are changes)
if messages and last_successful != current_commit:
repo_url = get_repo_url()
changelog_link = f"{repo_url}/compare/{last_successful}...{current_commit}"
new_section += f"\n{HEADER_MARKER} Full Changelog: [{last_successful[:8]}...{current_commit[:8]}]({changelog_link})\n"
return new_section
def update_release_md(existing_content, new_section, tag):
"""
Update input based on rules:
@@ -334,4 +378,4 @@ def main():
print(new_section)
if __name__ == "__main__":
main()
main()

View File

@@ -0,0 +1,3 @@
#include "../Build.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = $(PRODUCT_BUNDLE_IDENTIFIER).DataStructureTests