mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
Compare commits
9 Commits
alpha
...
ny/fix/703
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2f81e4ceb | ||
|
|
05ccccded8 | ||
|
|
950cb7f5b0 | ||
|
|
7f94edc041 | ||
|
|
62f91ec7a9 | ||
|
|
22fe96f923 | ||
|
|
10552780b1 | ||
|
|
daa82d00cf | ||
|
|
b0dd0bb416 |
200
.github/workflows/reusable-build-workflow.yml
vendored
200
.github/workflows/reusable-build-workflow.yml
vendored
@@ -45,15 +45,17 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
serialize:
|
common:
|
||||||
name: Wait for other jobs
|
name: Wait for other jobs
|
||||||
concurrency:
|
# 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
|
||||||
group: build-number-increment # serialize for build num cache access
|
# so we serialize on the entire workflow
|
||||||
|
concurrency:
|
||||||
|
group: serialize-workflow
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
runs-on: 'macos-15'
|
runs-on: 'macos-15'
|
||||||
steps:
|
steps:
|
||||||
- run: echo "No other contending jobs are running now...Build is ready to start"
|
- run: echo "No other contending jobs are running now..."
|
||||||
- name: Set short commit hash
|
- name: Set short commit hash
|
||||||
id: commit-id
|
id: commit-id
|
||||||
run: |
|
run: |
|
||||||
@@ -66,7 +68,7 @@ jobs:
|
|||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build SideStore - ${{ inputs.release_tag }}
|
name: Build SideStore - ${{ inputs.release_tag }}
|
||||||
needs: serialize
|
needs: common
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -88,6 +90,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install dependencies - ldid & xcbeautify
|
- name: Install dependencies - ldid & xcbeautify
|
||||||
run: |
|
run: |
|
||||||
@@ -157,7 +160,7 @@ jobs:
|
|||||||
build_num=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/.*\.([0-9]+)\+.*/\1/')
|
build_num=$(echo "${{ steps.version.outputs.version }}" | sed -E 's/.*\.([0-9]+)\+.*/\1/')
|
||||||
|
|
||||||
# Combine them into the final output
|
# Combine them into the final output
|
||||||
MARKETING_VERSION="${version}-${date}.${build_num}+${{ needs.serialize.outputs.short-commit }}"
|
MARKETING_VERSION="${version}-${date}.${build_num}+${{ needs.common.outputs.short-commit }}"
|
||||||
|
|
||||||
echo "MARKETING_VERSION=$MARKETING_VERSION" >> $GITHUB_ENV
|
echo "MARKETING_VERSION=$MARKETING_VERSION" >> $GITHUB_ENV
|
||||||
echo "MARKETING_VERSION=$MARKETING_VERSION" >> $GITHUB_OUTPUT
|
echo "MARKETING_VERSION=$MARKETING_VERSION" >> $GITHUB_OUTPUT
|
||||||
@@ -177,11 +180,11 @@ jobs:
|
|||||||
- name: (Build) Cache Build
|
- name: (Build) Cache Build
|
||||||
uses: irgaly/xcode-cache@v1
|
uses: irgaly/xcode-cache@v1
|
||||||
with:
|
with:
|
||||||
key: xcode-cache-deriveddata-build-${{ github.sha }}
|
key: xcode-cache-deriveddata-build-${{ github.ref_name }}-${{ github.sha }}
|
||||||
restore-keys: xcode-cache-deriveddata-build-
|
restore-keys: xcode-cache-deriveddata-build-${{ github.ref_name }}-
|
||||||
swiftpm-cache-key: xcode-cache-sourcedata-build-${{ github.sha }}
|
swiftpm-cache-key: xcode-cache-sourcedata-build-${{ github.ref_name }}-${{ github.sha }}
|
||||||
swiftpm-cache-restore-keys: |
|
swiftpm-cache-restore-keys: |
|
||||||
xcode-cache-sourcedata-build-
|
xcode-cache-sourcedata-build-${{ github.ref_name }}-
|
||||||
|
|
||||||
- name: (Build) Restore Pods from Cache (Exact match)
|
- name: (Build) Restore Pods from Cache (Exact match)
|
||||||
id: pods-restore
|
id: pods-restore
|
||||||
@@ -191,7 +194,7 @@ jobs:
|
|||||||
./Podfile.lock
|
./Podfile.lock
|
||||||
./Pods/
|
./Pods/
|
||||||
./AltStore.xcworkspace/
|
./AltStore.xcworkspace/
|
||||||
key: pods-cache-build-${{ hashFiles('Podfile') }}
|
key: pods-cache-build-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
|
||||||
# restore-keys: | # commented out to strictly check cache for this particular podfile
|
# restore-keys: | # commented out to strictly check cache for this particular podfile
|
||||||
# pods-cache-
|
# pods-cache-
|
||||||
|
|
||||||
@@ -204,7 +207,7 @@ jobs:
|
|||||||
./Podfile.lock
|
./Podfile.lock
|
||||||
./Pods/
|
./Pods/
|
||||||
./AltStore.xcworkspace/
|
./AltStore.xcworkspace/
|
||||||
key: pods-cache-build-
|
key: pods-cache-build-${{ github.ref_name }}-
|
||||||
|
|
||||||
|
|
||||||
- name: (Build) Install CocoaPods
|
- name: (Build) Install CocoaPods
|
||||||
@@ -219,7 +222,7 @@ jobs:
|
|||||||
./Podfile.lock
|
./Podfile.lock
|
||||||
./Pods/
|
./Pods/
|
||||||
./AltStore.xcworkspace/
|
./AltStore.xcworkspace/
|
||||||
key: pods-cache-build-${{ hashFiles('Podfile') }}
|
key: pods-cache-build-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
|
||||||
|
|
||||||
- name: (Build) Clean previous build artifacts
|
- name: (Build) Clean previous build artifacts
|
||||||
# using 'tee' to intercept stdout and log for detailed build-log
|
# using 'tee' to intercept stdout and log for detailed build-log
|
||||||
@@ -322,21 +325,70 @@ jobs:
|
|||||||
name: SideStore-${{ steps.version.outputs.version }}-dSYMs.zip
|
name: SideStore-${{ steps.version.outputs.version }}-dSYMs.zip
|
||||||
path: SideStore.dSYMs.zip
|
path: SideStore.dSYMs.zip
|
||||||
|
|
||||||
- name: Zip beta-beta-build-num & update_apps.py
|
- name: Keep rolling the build numbers for each successful build
|
||||||
run: |
|
|
||||||
zip -r -9 ./beta-build-num.zip ./SideStore/beta-build-num update_apps.py
|
|
||||||
|
|
||||||
- name: Upload beta-build-num artifact
|
|
||||||
if: ${{ inputs.is_beta }}
|
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 }} - ${{ needs.common.outputs.short-commit }} deployment" || echo "No changes to commit"
|
||||||
|
|
||||||
|
echo "Pushing to remote repo"
|
||||||
|
git push --verbose
|
||||||
|
popd
|
||||||
|
|
||||||
|
- 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 }}
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- name: Upload release-notes.md
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: beta-build-num-${{ steps.version.outputs.version }}.zip
|
name: release-notes-${{ needs.common.outputs.short-commit }}.md
|
||||||
path: beta-build-num.zip
|
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
|
||||||
|
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
|
||||||
|
path: update_apps.py
|
||||||
|
|
||||||
tests-build:
|
tests-build:
|
||||||
name: Tests-Build SideStore - ${{ inputs.release_tag }}
|
name: Tests-Build SideStore - ${{ inputs.release_tag }}
|
||||||
needs: serialize
|
needs: common
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -363,11 +415,11 @@ jobs:
|
|||||||
- name: (Tests-Build) Cache Build
|
- name: (Tests-Build) Cache Build
|
||||||
uses: irgaly/xcode-cache@v1
|
uses: irgaly/xcode-cache@v1
|
||||||
with:
|
with:
|
||||||
key: xcode-cache-deriveddata-test-${{ github.sha }}
|
key: xcode-cache-deriveddata-test-${{ github.ref_name }}-${{ github.sha }}
|
||||||
restore-keys: xcode-cache-deriveddata-test-
|
restore-keys: xcode-cache-deriveddata-test-${{ github.ref_name }}-
|
||||||
swiftpm-cache-key: xcode-cache-sourcedata-test-${{ github.sha }}
|
swiftpm-cache-key: xcode-cache-sourcedata-test-${{ github.ref_name }}-${{ github.sha }}
|
||||||
swiftpm-cache-restore-keys: |
|
swiftpm-cache-restore-keys: |
|
||||||
xcode-cache-sourcedata-test-
|
xcode-cache-sourcedata-test-${{ github.ref_name }}-
|
||||||
|
|
||||||
- name: (Tests-Build) Restore Pods from Cache (Exact match)
|
- name: (Tests-Build) Restore Pods from Cache (Exact match)
|
||||||
id: pods-restore
|
id: pods-restore
|
||||||
@@ -377,7 +429,7 @@ jobs:
|
|||||||
./Podfile.lock
|
./Podfile.lock
|
||||||
./Pods/
|
./Pods/
|
||||||
./AltStore.xcworkspace/
|
./AltStore.xcworkspace/
|
||||||
key: pods-cache-test-${{ hashFiles('Podfile') }}
|
key: pods-cache-test-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
|
||||||
|
|
||||||
- name: (Tests-Build) Restore Pods from Cache (Last Available)
|
- name: (Tests-Build) Restore Pods from Cache (Last Available)
|
||||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||||
@@ -388,7 +440,7 @@ jobs:
|
|||||||
./Podfile.lock
|
./Podfile.lock
|
||||||
./Pods/
|
./Pods/
|
||||||
./AltStore.xcworkspace/
|
./AltStore.xcworkspace/
|
||||||
key: pods-cache-test-
|
key: pods-cache-test-${{ github.ref_name }}-
|
||||||
|
|
||||||
- name: (Tests-Build) Install CocoaPods
|
- name: (Tests-Build) Install CocoaPods
|
||||||
run: pod install
|
run: pod install
|
||||||
@@ -401,7 +453,7 @@ jobs:
|
|||||||
./Podfile.lock
|
./Podfile.lock
|
||||||
./Pods/
|
./Pods/
|
||||||
./AltStore.xcworkspace/
|
./AltStore.xcworkspace/
|
||||||
key: pods-cache-test-${{ hashFiles('Podfile') }}
|
key: pods-cache-test-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
|
||||||
|
|
||||||
- name: (Tests-Build) Clean previous build artifacts
|
- name: (Tests-Build) Clean previous build artifacts
|
||||||
run: |
|
run: |
|
||||||
@@ -467,12 +519,12 @@ jobs:
|
|||||||
if: always() && steps.encrypt-test-log.outputs.encrypted == 'true'
|
if: always() && steps.encrypt-test-log.outputs.encrypted == 'true'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: encrypted-tests-build-logs-${{ needs.serialize.outputs.short-commit }}.zip
|
name: encrypted-tests-build-logs-${{ needs.common.outputs.short-commit }}.zip
|
||||||
path: encrypted-tests-build-logs.zip
|
path: encrypted-tests-build-logs.zip
|
||||||
|
|
||||||
tests-run:
|
tests-run:
|
||||||
name: Tests-Run SideStore - ${{ inputs.release_tag }}
|
name: Tests-Run SideStore - ${{ inputs.release_tag }}
|
||||||
needs: [serialize, tests-build]
|
needs: [common, tests-build]
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -487,11 +539,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- name: Boot Simulator for testing
|
- name: Boot Simulator async(nohup) for testing
|
||||||
run: |
|
run: |
|
||||||
mkdir -p build/logs
|
mkdir -p build/logs
|
||||||
make -B boot-sim-async | tee -a build/logs/tests-run.log
|
nohup make -B boot-sim-async </dev/null >> build/logs/tests-run.log 2>&1 &
|
||||||
exit ${PIPESTATUS[0]}
|
|
||||||
|
|
||||||
- name: Setup Xcode
|
- name: Setup Xcode
|
||||||
uses: maxim-lobanov/setup-xcode@v1.6.0
|
uses: maxim-lobanov/setup-xcode@v1.6.0
|
||||||
@@ -501,11 +552,11 @@ jobs:
|
|||||||
- name: (Tests-Run) Cache Build
|
- name: (Tests-Run) Cache Build
|
||||||
uses: irgaly/xcode-cache@v1
|
uses: irgaly/xcode-cache@v1
|
||||||
with:
|
with:
|
||||||
key: xcode-cache-deriveddata-test-${{ github.sha }}
|
key: xcode-cache-deriveddata-test-${{ github.ref_name }}-${{ github.sha }}
|
||||||
restore-keys: xcode-cache-deriveddata-test-
|
restore-keys: xcode-cache-deriveddata-test-${{ github.ref_name }}-
|
||||||
swiftpm-cache-key: xcode-cache-sourcedata-test-${{ github.sha }}
|
swiftpm-cache-key: xcode-cache-sourcedata-test-${{ github.ref_name }}-${{ github.sha }}
|
||||||
swiftpm-cache-restore-keys: |
|
swiftpm-cache-restore-keys: |
|
||||||
xcode-cache-sourcedata-test-
|
xcode-cache-sourcedata-test-${{ github.ref_name }}-
|
||||||
|
|
||||||
- name: (Tests-Run) Restore Pods from Cache (Exact match)
|
- name: (Tests-Run) Restore Pods from Cache (Exact match)
|
||||||
id: pods-restore
|
id: pods-restore
|
||||||
@@ -515,7 +566,7 @@ jobs:
|
|||||||
./Podfile.lock
|
./Podfile.lock
|
||||||
./Pods/
|
./Pods/
|
||||||
./AltStore.xcworkspace/
|
./AltStore.xcworkspace/
|
||||||
key: pods-cache-test-${{ hashFiles('Podfile') }}
|
key: pods-cache-test-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
|
||||||
|
|
||||||
- name: (Tests-Run) Restore Pods from Cache (Last Available)
|
- name: (Tests-Run) Restore Pods from Cache (Last Available)
|
||||||
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
if: ${{ steps.pods-restore.outputs.cache-hit != 'true' }}
|
||||||
@@ -526,7 +577,7 @@ jobs:
|
|||||||
./Podfile.lock
|
./Podfile.lock
|
||||||
./Pods/
|
./Pods/
|
||||||
./AltStore.xcworkspace/
|
./AltStore.xcworkspace/
|
||||||
key: pods-cache-test-
|
key: pods-cache-test-${{ github.ref_name }}-
|
||||||
|
|
||||||
- name: (Tests-Run) Install CocoaPods
|
- name: (Tests-Run) Install CocoaPods
|
||||||
run: pod install
|
run: pod install
|
||||||
@@ -539,7 +590,7 @@ jobs:
|
|||||||
./Podfile.lock
|
./Podfile.lock
|
||||||
./Pods/
|
./Pods/
|
||||||
./AltStore.xcworkspace/
|
./AltStore.xcworkspace/
|
||||||
key: pods-cache-test-${{ hashFiles('Podfile') }}
|
key: pods-cache-test-${{ github.ref_name }}-${{ hashFiles('Podfile') }}
|
||||||
|
|
||||||
- name: (Tests-Run) Clean previous build artifacts
|
- name: (Tests-Run) Clean previous build artifacts
|
||||||
run: |
|
run: |
|
||||||
@@ -625,7 +676,7 @@ jobs:
|
|||||||
if: always() && steps.encrypt-test-log.outputs.encrypted == 'true'
|
if: always() && steps.encrypt-test-log.outputs.encrypted == 'true'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: encrypted-tests-run-logs-${{ needs.serialize.outputs.short-commit }}.zip
|
name: encrypted-tests-run-logs-${{ needs.common.outputs.short-commit }}.zip
|
||||||
path: encrypted-tests-run-logs.zip
|
path: encrypted-tests-run-logs.zip
|
||||||
|
|
||||||
- name: Print tests-recording.log contents (if exists)
|
- name: Print tests-recording.log contents (if exists)
|
||||||
@@ -655,7 +706,7 @@ jobs:
|
|||||||
if: ${{ always() && steps.check-recording.outputs.found == 'true' }}
|
if: ${{ always() && steps.check-recording.outputs.found == 'true' }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: tests-recording-${{ needs.serialize.outputs.short-commit }}.mp4
|
name: tests-recording-${{ needs.common.outputs.short-commit }}.mp4
|
||||||
path: tests-recording.mp4
|
path: tests-recording.mp4
|
||||||
|
|
||||||
- name: Zip test-results
|
- name: Zip test-results
|
||||||
@@ -664,14 +715,14 @@ jobs:
|
|||||||
- name: Upload Test Artifacts
|
- name: Upload Test Artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: test-results-${{ needs.serialize.outputs.short-commit }}.zip
|
name: test-results-${{ needs.common.outputs.short-commit }}.zip
|
||||||
path: test-results.zip
|
path: test-results.zip
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
name: Deploy SideStore - ${{ inputs.release_tag }}
|
name: Deploy SideStore - ${{ inputs.release_tag }}
|
||||||
runs-on: macos-15
|
runs-on: macos-15
|
||||||
# needs: [serialize, build]
|
# needs: [common, build]
|
||||||
needs: [serialize, build, tests-build, tests-run]
|
needs: [common, build, tests-build, tests-run]
|
||||||
steps:
|
steps:
|
||||||
- name: Download IPA artifact
|
- name: Download IPA artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
@@ -691,32 +742,45 @@ jobs:
|
|||||||
- name: Download encrypted-tests-build-logs artifact
|
- name: Download encrypted-tests-build-logs artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: encrypted-tests-build-logs-${{ needs.serialize.outputs.short-commit }}.zip
|
name: encrypted-tests-build-logs-${{ needs.common.outputs.short-commit }}.zip
|
||||||
|
|
||||||
- name: Download encrypted-tests-run-logs artifact
|
- name: Download encrypted-tests-run-logs artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: encrypted-tests-run-logs-${{ needs.serialize.outputs.short-commit }}.zip
|
name: encrypted-tests-run-logs-${{ needs.common.outputs.short-commit }}.zip
|
||||||
|
|
||||||
- name: Download tests-recording artifact
|
- name: Download tests-recording artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: tests-recording-${{ needs.serialize.outputs.short-commit }}.mp4
|
name: tests-recording-${{ needs.common.outputs.short-commit }}.mp4
|
||||||
|
|
||||||
- name: Download test-results artifact
|
- name: Download test-results artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: test-results-${{ needs.serialize.outputs.short-commit }}.zip
|
name: test-results-${{ needs.common.outputs.short-commit }}.zip
|
||||||
|
|
||||||
- name: Download beta-build-num artifact
|
- name: Download release-notes.md
|
||||||
if: ${{ inputs.is_beta }}
|
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: beta-build-num-${{ needs.build.outputs.version }}.zip
|
name: release-notes-${{ needs.common.outputs.short-commit }}.md
|
||||||
- name: Un-Zip beta-beta-build-num & update_apps.py
|
|
||||||
|
- name: Download update_release_notes.py
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: update_release_notes-${{ needs.common.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: Read release notes
|
||||||
|
id: release_notes
|
||||||
run: |
|
run: |
|
||||||
unzip beta-build-num.zip -d .
|
CONTENT=$(python3 update_release_notes.py --retrieve ${{ inputs.release_tag }})
|
||||||
|
echo "content<<EOF" >> $GITHUB_OUTPUT
|
||||||
|
echo "$CONTENT" >> $GITHUB_OUTPUT
|
||||||
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: List files before upload
|
- name: List files before upload
|
||||||
run: |
|
run: |
|
||||||
@@ -754,22 +818,7 @@ jobs:
|
|||||||
Commit SHA: `${{ github.sha }}`
|
Commit SHA: `${{ github.sha }}`
|
||||||
Version: `${{ needs.build.outputs.version }}`
|
Version: `${{ needs.build.outputs.version }}`
|
||||||
|
|
||||||
- name: Publish to SideStore/beta-build-num
|
${{ steps.release_notes.outputs.content }}
|
||||||
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 }} - ${{ needs.serialize.outputs.short-commit }} deployment" || echo "No changes to commit"
|
|
||||||
|
|
||||||
echo "Pushing to remote repo"
|
|
||||||
git push --verbose
|
|
||||||
popd
|
|
||||||
|
|
||||||
- name: Get formatted date
|
- name: Get formatted date
|
||||||
run: |
|
run: |
|
||||||
@@ -801,8 +850,11 @@ jobs:
|
|||||||
LOCALIZED_DESCRIPTION=$(cat <<EOF
|
LOCALIZED_DESCRIPTION=$(cat <<EOF
|
||||||
This is release for:
|
This is release for:
|
||||||
- version: "${{ needs.build.outputs.version }}"
|
- version: "${{ needs.build.outputs.version }}"
|
||||||
- revision: "${{ needs.serialize.outputs.short-commit }}"
|
- revision: "${{ needs.common.outputs.short-commit }}"
|
||||||
- timestamp: "${{ steps.date.outputs.date }}"
|
- timestamp: "${{ steps.date.outputs.date }}"
|
||||||
|
|
||||||
|
Release Notes:
|
||||||
|
${{ steps.release_notes.outputs.content }}
|
||||||
EOF
|
EOF
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -851,7 +903,7 @@ jobs:
|
|||||||
|
|
||||||
# Commit changes and push using SSH
|
# Commit changes and push using SSH
|
||||||
git add --verbose ./_includes/source.json
|
git add --verbose ./_includes/source.json
|
||||||
git commit -m " - updated for ${{ needs.serialize.outputs.short-commit }} deployment" || echo "No changes to commit"
|
git commit -m " - updated for ${{ needs.common.outputs.short-commit }} deployment" || echo "No changes to commit"
|
||||||
|
|
||||||
git push --verbose
|
git push --verbose
|
||||||
popd
|
popd
|
||||||
|
|||||||
@@ -242,6 +242,7 @@
|
|||||||
<key>public.filename-extension</key>
|
<key>public.filename-extension</key>
|
||||||
<array>
|
<array>
|
||||||
<string>mobiledevicepairing</string>
|
<string>mobiledevicepairing</string>
|
||||||
|
<string>mobiledevicepair</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
337
update_release_notes.py
Normal file
337
update_release_notes.py
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
IGNORED_AUTHORS = [
|
||||||
|
"=", # probably someone used an equalTo ?! # anyway we are ignoring it!
|
||||||
|
]
|
||||||
|
|
||||||
|
TAG_MARKER = "###"
|
||||||
|
HEADER_MARKER = "####"
|
||||||
|
|
||||||
|
def run_command(cmd):
|
||||||
|
"""Run a shell command and return its trimmed output."""
|
||||||
|
return subprocess.check_output(cmd, shell=True, text=True).strip()
|
||||||
|
|
||||||
|
def get_head_commit():
|
||||||
|
"""Return the HEAD commit SHA."""
|
||||||
|
return run_command("git rev-parse HEAD")
|
||||||
|
|
||||||
|
def get_commit_messages(last_successful, current="HEAD"):
|
||||||
|
"""Return a list of commit messages between last_successful and current."""
|
||||||
|
cmd = f"git log {last_successful}..{current} --pretty=format:%s"
|
||||||
|
output = run_command(cmd)
|
||||||
|
if not output:
|
||||||
|
return []
|
||||||
|
return output.splitlines()
|
||||||
|
|
||||||
|
def get_authors_in_range(commit_range, fmt="%an"):
|
||||||
|
"""Return a set of commit authors in the given commit range using the given format."""
|
||||||
|
cmd = f"git log {commit_range} --pretty=format:{fmt}"
|
||||||
|
output = run_command(cmd)
|
||||||
|
if not output:
|
||||||
|
return set()
|
||||||
|
authors = set(line.strip() for line in output.splitlines() if line.strip())
|
||||||
|
authors = set(authors) - set(IGNORED_AUTHORS)
|
||||||
|
return authors
|
||||||
|
|
||||||
|
def get_first_commit_of_repo():
|
||||||
|
"""Return the first commit in the repository (root commit)."""
|
||||||
|
cmd = "git rev-list --max-parents=0 HEAD"
|
||||||
|
output = run_command(cmd)
|
||||||
|
return output.splitlines()[0]
|
||||||
|
|
||||||
|
def get_branch():
|
||||||
|
"""
|
||||||
|
Attempt to determine the branch base (the commit where the current branch diverged
|
||||||
|
from the default remote branch). Falls back to the repo's first commit.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
default_ref = run_command("git rev-parse --abbrev-ref origin/HEAD")
|
||||||
|
default_branch = default_ref.split('/')[-1]
|
||||||
|
base_commit = run_command(f"git merge-base HEAD origin/{default_branch}")
|
||||||
|
return base_commit
|
||||||
|
except Exception:
|
||||||
|
return get_first_commit_of_repo()
|
||||||
|
|
||||||
|
def get_repo_url():
|
||||||
|
"""Extract and clean the repository URL from the remote 'origin'."""
|
||||||
|
url = run_command("git config --get remote.origin.url")
|
||||||
|
if url.startswith("git@"):
|
||||||
|
url = url.replace("git@", "https://").replace(":", "/")
|
||||||
|
if url.endswith(".git"):
|
||||||
|
url = url[:-4]
|
||||||
|
return url
|
||||||
|
|
||||||
|
def format_contributor(author):
|
||||||
|
"""
|
||||||
|
Convert an author name to a GitHub username or first name.
|
||||||
|
If the author already starts with '@', return it;
|
||||||
|
otherwise, take the first token and prepend '@'.
|
||||||
|
"""
|
||||||
|
if author.startswith('@'):
|
||||||
|
return author
|
||||||
|
return f"@{author.split()[0]}"
|
||||||
|
|
||||||
|
def format_commit_message(msg):
|
||||||
|
"""Format a commit message as a bullet point for the release notes."""
|
||||||
|
msg_clean = msg.lstrip() # remove leading spaces
|
||||||
|
if msg_clean.startswith("-"):
|
||||||
|
msg_clean = msg_clean[1:].strip() # remove leading '-' and spaces
|
||||||
|
return f"- {msg_clean}"
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
1. If tag exists, update it
|
||||||
|
2. Special tags (alpha, beta, nightly) stay at the top in that order
|
||||||
|
3. Numbered tags follow special tags
|
||||||
|
4. Remove duplicate tags
|
||||||
|
5. Insert new numbered tags at the top of the numbered section
|
||||||
|
"""
|
||||||
|
tag_lower = tag.lower()
|
||||||
|
is_special_tag = tag_lower in ["alpha", "beta", "nightly"]
|
||||||
|
|
||||||
|
# Parse the existing content into sections
|
||||||
|
if not existing_content:
|
||||||
|
return new_section
|
||||||
|
|
||||||
|
# Split the content into sections by headers
|
||||||
|
pattern = fr'(^{TAG_MARKER} .*$)'
|
||||||
|
sections = re.split(pattern, existing_content, flags=re.MULTILINE)
|
||||||
|
|
||||||
|
# Create a list to store the processed content
|
||||||
|
processed_sections = []
|
||||||
|
|
||||||
|
# Track special tag positions and whether tag was found
|
||||||
|
special_tags_map = {"alpha": False, "beta": False, "nightly": False}
|
||||||
|
last_special_index = -1
|
||||||
|
tag_found = False
|
||||||
|
numbered_tag_index = -1
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
while i < len(sections):
|
||||||
|
# Check if this is a header
|
||||||
|
if i % 2 == 1: # Headers are at odd indices
|
||||||
|
header = sections[i]
|
||||||
|
content = sections[i+1] if i+1 < len(sections) else ""
|
||||||
|
current_tag = header[3:].strip().lower()
|
||||||
|
|
||||||
|
# Check for special tags to track their positions
|
||||||
|
if current_tag in special_tags_map:
|
||||||
|
special_tags_map[current_tag] = True
|
||||||
|
last_special_index = len(processed_sections)
|
||||||
|
|
||||||
|
# Check if this is the first numbered tag
|
||||||
|
elif re.match(r'^[0-9]+\.[0-9]+(\.[0-9]+)?$', current_tag) and numbered_tag_index == -1:
|
||||||
|
numbered_tag_index = len(processed_sections)
|
||||||
|
|
||||||
|
# If this is the tag we're updating, mark it but don't add yet
|
||||||
|
if current_tag == tag_lower:
|
||||||
|
if not tag_found: # Replace the first occurrence
|
||||||
|
tag_found = True
|
||||||
|
i += 2 # Skip the content
|
||||||
|
continue
|
||||||
|
else: # Skip duplicate occurrences
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Add the current section
|
||||||
|
processed_sections.append(sections[i])
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# Determine where to insert the new section
|
||||||
|
if tag_found:
|
||||||
|
# We need to determine the insertion point
|
||||||
|
if is_special_tag:
|
||||||
|
# For special tags, insert after last special tag or at beginning
|
||||||
|
desired_index = -1
|
||||||
|
for pos, t in enumerate(["alpha", "beta", "nightly"]):
|
||||||
|
if t == tag_lower:
|
||||||
|
desired_index = pos
|
||||||
|
|
||||||
|
# Find position to insert
|
||||||
|
insert_pos = 0
|
||||||
|
for pos, t in enumerate(["alpha", "beta", "nightly"]):
|
||||||
|
if t == tag_lower:
|
||||||
|
break
|
||||||
|
if special_tags_map[t]:
|
||||||
|
insert_pos = processed_sections.index(f"{TAG_MARKER} {t}")
|
||||||
|
insert_pos += 2 # Move past the header and content
|
||||||
|
|
||||||
|
# Insert at the determined position
|
||||||
|
processed_sections.insert(insert_pos, new_section)
|
||||||
|
if insert_pos > 0 and not processed_sections[insert_pos-1].endswith('\n\n'):
|
||||||
|
processed_sections.insert(insert_pos, '\n\n')
|
||||||
|
else:
|
||||||
|
# For numbered tags, insert after special tags but before other numbered tags
|
||||||
|
insert_pos = 0
|
||||||
|
|
||||||
|
if last_special_index >= 0:
|
||||||
|
# Insert after the last special tag
|
||||||
|
insert_pos = last_special_index + 2 # +2 to skip header and content
|
||||||
|
|
||||||
|
processed_sections.insert(insert_pos, new_section)
|
||||||
|
if insert_pos > 0 and not processed_sections[insert_pos-1].endswith('\n\n'):
|
||||||
|
processed_sections.insert(insert_pos, '\n\n')
|
||||||
|
else:
|
||||||
|
# Tag doesn't exist yet, determine insertion point
|
||||||
|
if is_special_tag:
|
||||||
|
# For special tags, maintain alpha, beta, nightly order
|
||||||
|
special_tags = ["alpha", "beta", "nightly"]
|
||||||
|
insert_pos = 0
|
||||||
|
|
||||||
|
for i, t in enumerate(special_tags):
|
||||||
|
if t == tag_lower:
|
||||||
|
# Check if preceding special tags exist
|
||||||
|
for prev_tag in special_tags[:i]:
|
||||||
|
if special_tags_map[prev_tag]:
|
||||||
|
# Find the position after this tag
|
||||||
|
prev_index = processed_sections.index(f"{TAG_MARKER} {prev_tag}")
|
||||||
|
insert_pos = prev_index + 2 # Skip header and content
|
||||||
|
|
||||||
|
processed_sections.insert(insert_pos, new_section)
|
||||||
|
if insert_pos > 0 and not processed_sections[insert_pos-1].endswith('\n\n'):
|
||||||
|
processed_sections.insert(insert_pos, '\n\n')
|
||||||
|
else:
|
||||||
|
# For numbered tags, insert after special tags but before other numbered tags
|
||||||
|
insert_pos = 0
|
||||||
|
|
||||||
|
if last_special_index >= 0:
|
||||||
|
# Insert after the last special tag
|
||||||
|
insert_pos = last_special_index + 2 # +2 to skip header and content
|
||||||
|
|
||||||
|
processed_sections.insert(insert_pos, new_section)
|
||||||
|
if insert_pos > 0 and not processed_sections[insert_pos-1].endswith('\n\n'):
|
||||||
|
processed_sections.insert(insert_pos, '\n\n')
|
||||||
|
|
||||||
|
# Combine sections ensuring proper spacing
|
||||||
|
result = ""
|
||||||
|
for i, section in enumerate(processed_sections):
|
||||||
|
if i > 0 and section.startswith(f"{TAG_MARKER} "):
|
||||||
|
# Ensure single blank line before headers
|
||||||
|
if not result.endswith("\n\n"):
|
||||||
|
result = result.rstrip("\n") + "\n\n"
|
||||||
|
result += section
|
||||||
|
|
||||||
|
return result.rstrip() + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def retrieve_tag_content(tag, file_path):
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
with open(file_path, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Create a pattern for the tag header (case-insensitive)
|
||||||
|
pattern = re.compile(fr'^{TAG_MARKER} ' + re.escape(tag) + r'$', re.MULTILINE | re.IGNORECASE)
|
||||||
|
|
||||||
|
# Find the tag header
|
||||||
|
match = pattern.search(content)
|
||||||
|
if not match:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Start after the tag line
|
||||||
|
start_pos = match.end()
|
||||||
|
|
||||||
|
# Skip a newline if present
|
||||||
|
if start_pos < len(content) and content[start_pos] == "\n":
|
||||||
|
start_pos += 1
|
||||||
|
|
||||||
|
# Find the next tag header after the current tag's content
|
||||||
|
next_tag_match = re.search(fr'^{TAG_MARKER} ', content[start_pos:], re.MULTILINE)
|
||||||
|
|
||||||
|
if next_tag_match:
|
||||||
|
end_pos = start_pos + next_tag_match.start()
|
||||||
|
return content[start_pos:end_pos].strip()
|
||||||
|
else:
|
||||||
|
# Return until the end of the file if this is the last tag
|
||||||
|
return content[start_pos:].strip()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Update input file
|
||||||
|
release_file = "release-notes.md"
|
||||||
|
|
||||||
|
# Usage: python release.py <last_successful_commit> [tag] [branch]
|
||||||
|
# Or: python release.py --retrieve <tagname>
|
||||||
|
args = sys.argv[1:]
|
||||||
|
|
||||||
|
if len(args) < 1:
|
||||||
|
print("Usage: python release.py <last_successful_commit> [tag] [branch]")
|
||||||
|
print(" or: python release.py --retrieve <tagname>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Check if we're retrieving a tag
|
||||||
|
if args[0] == "--retrieve":
|
||||||
|
if len(args) < 2:
|
||||||
|
print("Error: Missing tag name after --retrieve")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
tag_content = retrieve_tag_content(args[1], file_path=release_file)
|
||||||
|
if tag_content:
|
||||||
|
print(tag_content)
|
||||||
|
else:
|
||||||
|
print(f"Tag '{args[1]}' not found in '{release_file}'")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Original functionality for generating release notes
|
||||||
|
last_successful = args[0]
|
||||||
|
tag = args[1] if len(args) > 1 else get_head_commit()
|
||||||
|
branch = args[2] if len(args) > 2 else (os.environ.get("GITHUB_REF") or get_branch())
|
||||||
|
|
||||||
|
# Generate release notes
|
||||||
|
new_section = generate_release_notes(last_successful, tag, branch)
|
||||||
|
|
||||||
|
existing_content = ""
|
||||||
|
if os.path.exists(release_file):
|
||||||
|
with open(release_file, "r") as f:
|
||||||
|
existing_content = f.read()
|
||||||
|
|
||||||
|
updated_content = update_release_md(existing_content, new_section, tag)
|
||||||
|
|
||||||
|
with open(release_file, "w") as f:
|
||||||
|
f.write(updated_content)
|
||||||
|
|
||||||
|
# Output the new section for display
|
||||||
|
print(new_section)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user