Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
name: Build and Deploy

on:
push:
branches:
- main
pull_request:
branches:
- main
types:
- opened
- synchronize
- reopened
- ready_for_review

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }}
APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }}
APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }}
MATCH_KEYCHAIN_NAME: ${{ vars.MATCH_KEYCHAIN_NAME }}
MATCH_KEYCHAIN_PASSWORD: ${{ secrets.MATCH_KEYCHAIN_PASSWORD }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
FASTLANE_ENV: 'stg'
XCODE_VERSION: '26.5'

jobs:
build:
runs-on: macos-26
# Block execution in template repositories and skip draft PRs
if: github.event.pull_request.draft == false && !contains(github.repository, 'template')
timeout-minutes: 60
steps:
- uses: actions/checkout@v4

- name: Select Xcode version
run: sudo xcode-select -s '/Applications/Xcode_${{ env.XCODE_VERSION }}.app'

- name: Set up SSH key
run: |
set -e
mkdir -p ~/.ssh
echo "${{ secrets.HOPPSEN_BOT_SSH_KEY }}" > ~/.ssh/github_actions
chmod 0600 ~/.ssh/github_actions
ssh-add ~/.ssh/github_actions

# Disable Strict Host Key Checking in SSH
printf "Host *\n\tStrictHostKeyChecking no" > ~/.ssh/config
chmod 400 ~/.ssh/config

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3.0'
bundler-cache: true

- name: Cache SPM
id: cache_spm
uses: actions/cache@v4
with:
path: fastlane/sourcePackages
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-

- name: Cache Derived Data
id: cache_derived_data
uses: actions/cache@v4
with:
path: fastlane/derivedData
key: ${{ runner.os }}-xcode-${{ env.XCODE_VERSION }}-derived-data-${{ hashFiles('**/Package.resolved', '**/Template.xcodeproj') }}
restore-keys: |
${{ runner.os }}-xcode-${{ env.XCODE_VERSION }}-derived-data-

- name: Update version number
run: bundle exec fastlane updateVersion --env $FASTLANE_ENV

- name: Build
run: bundle exec fastlane build --env $FASTLANE_ENV
env:
SPM_CACHE_RESTORED: ${{ steps.cache_spm.outputs.cache-hit }}
FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 5
FASTLANE_XCODEBUILD_SETTINGS_RETRIES: 10

- name: Deploy to TestFlight
run: bundle exec fastlane deploy changelog:${{ github.head_ref || github.ref_name }} --env $FASTLANE_ENV

- name: Post TestFlight build number to Pull Request
uses: actions/github-script@v7
if: github.event.pull_request
with:
github-token: ${{ secrets.HOPPSEN_BOT_TOKEN }}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `**TestFlight build ${{ env.VERSION_NUMBER }} (${{ env.BUILD_NUMBER }})** has been successfully completed and is now processing. The build should be available shortly. 🚀📱`
})

- name: Upload output files
uses: actions/upload-artifact@v4
with:
name: outputs
path: |
culprits.txt
fastlane/output/*.ipa
fastlane/output/*.dSYM.zip
retention-days: 7

- name: Upload gym build log
if: failure()
uses: actions/upload-artifact@v4
with:
name: gym-log
path: /Users/runner/Library/Logs/gym/*.log
2 changes: 1 addition & 1 deletion .github/workflows/linting.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ on:
jobs:
SwiftLint:
runs-on: ubuntu-latest
timeout-minutes: 20
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Read version from Mintfile
Expand Down
132 changes: 132 additions & 0 deletions .github/workflows/releaseMonitor.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
name: Release Monitor

on:
# Disabled until the first release is created
# schedule:
# - cron: '0 * * * *' # Run every hour

# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

env:
APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }}
APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }}
APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }}
APP_STORE_APP_ID: ${{ vars.APP_STORE_APP_ID }} # <- CHANGE this to the app ID of the app you want to monitor

jobs:
app-store-release-monitor:
runs-on: ubuntu-latest
# Block execution in template repositories
if: ${{ !contains(github.repository, 'template') }}
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install jsonwebtoken
run: npm install jsonwebtoken

- name: Check App Store Version
uses: actions/github-script@v7
with:
github-token: ${{ secrets.HOPPSEN_BOT_TOKEN }}
script: |
const jwt = require('./node_modules/jsonwebtoken');

function generateToken() {
const privateKey = process.env.APP_STORE_CONNECT_API_KEY_KEY.replace(/\\n/g, '\n');
return jwt.sign({
iss: process.env.APP_STORE_CONNECT_API_KEY_ISSUER_ID,
exp: Math.floor(Date.now() / 1000) + (20 * 60), // 20 minutes
aud: 'appstoreconnect-v1'
}, privateKey, {
algorithm: 'ES256',
header: {
kid: process.env.APP_STORE_CONNECT_API_KEY_KEY_ID
}
});
}

async function getLatestAppStoreVersion() {
const token = generateToken();

console.log('::group::Version API Response');
const versionResponse = await fetch(
`https://api.appstoreconnect.apple.com/v1/apps/${process.env.APP_STORE_APP_ID}/appStoreVersions?filter[platform]=IOS&filter[appStoreState]=READY_FOR_SALE`,
{
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
);
const versionData = await versionResponse.json();
if (!versionResponse.ok) {
throw new Error(`Version API request failed: ${JSON.stringify(versionData)}`);
}

console.log(JSON.stringify(versionData.data[0], null, 2));
console.log('::endgroup::');

if (!versionData.data || !versionData.data[0]) {
throw new Error('No version data found');
}

// Get the build URL from relationships
const buildUrl = versionData.data[0].relationships.build.links.related;

// Get the build info
const buildResponse = await fetch(buildUrl, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
const buildData = await buildResponse.json();
if (!buildResponse.ok) {
throw new Error(`Build API request failed: ${JSON.stringify(buildData)}`);
}

console.log('::group::Build API Response');
console.log(JSON.stringify(buildData, null, 2));
console.log('::endgroup::');

return {
version: versionData.data[0].attributes.versionString,
buildNumber: buildData.data.attributes.version
};
}

try {
const appInfo = await getLatestAppStoreVersion();
const tagName = `${appInfo.version}-${appInfo.buildNumber}`;

// Check if release already exists
try {
await github.rest.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag: tagName
});
console.log(`Release ${tagName} already exists, skipping creation`);
return;
} catch (e) {
// Release doesn't exist, create it
console.log(`Creating new release for ${tagName}`);

// Create release with auto-generated notes
await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: tagName,
name: `${tagName}`,
generate_release_notes: true
});

console.log(`Successfully created release for version ${tagName}`);
}
} catch (error) {
core.setFailed(`Release creation failed: ${error.message}`);
}
119 changes: 119 additions & 0 deletions .github/workflows/releasing.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
name: Releasing

on:
# schedule:
# - cron: '0 16 * * FRI' # At 20:00 on Friday Dubai time

# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
inputs:
project:
description: 'Select the project to point to.'
required: true
default: 'prd'
type: choice
options:
- stg
- prd
patch_number:
description: 'Specify the patch number to be appended to the version number in the following format: <year>.<week>.<patch> Only needed when releasing more than one version in a week.'
required: false
default: '0'

env:
APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }}
APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }}
APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }}
MATCH_KEYCHAIN_NAME: ${{ vars.MATCH_KEYCHAIN_NAME }}
MATCH_KEYCHAIN_PASSWORD: ${{ secrets.MATCH_KEYCHAIN_PASSWORD }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
FASTLANE_ENV: ${{ inputs.project || 'prd' }}
PATCH_NUMBER: ${{ inputs.patch_number || '0' }}
XCODE_VERSION: '26.5'

jobs:
build:
runs-on: macos-26
# Block execution in template repositories
if: ${{ !contains(github.repository, 'template') }}
timeout-minutes: 60
steps:
- uses: actions/checkout@v4

- name: Select Xcode version
run: sudo xcode-select -s '/Applications/Xcode_${{ env.XCODE_VERSION }}.app'

- name: Set up SSH key
run: |
set -e
mkdir -p ~/.ssh
echo "${{ secrets.HOPPSEN_BOT_SSH_KEY }}" > ~/.ssh/github_actions
chmod 0600 ~/.ssh/github_actions
ssh-add ~/.ssh/github_actions

# Disable Strict Host Key Checking in SSH
printf "Host *\n\tStrictHostKeyChecking no" > ~/.ssh/config
chmod 400 ~/.ssh/config

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3.0'
bundler-cache: true

- name: Cache SPM
id: cache_spm
uses: actions/cache@v4
with:
path: fastlane/sourcePackages
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-

- name: Cache Derived Data
id: cache_derived_data
uses: actions/cache@v4
with:
path: fastlane/derivedData
key: ${{ runner.os }}-xcode-${{ env.XCODE_VERSION }}-derived-data-${{ hashFiles('**/Package.resolved', '**/Template.xcodeproj') }}
restore-keys: |
${{ runner.os }}-xcode-${{ env.XCODE_VERSION }}-derived-data-

- name: Update version number
run: bundle exec fastlane updateVersion patch_number:$PATCH_NUMBER --env $FASTLANE_ENV

- name: Build
run: bundle exec fastlane build --env $FASTLANE_ENV
env:
SPM_CACHE_RESTORED: ${{ steps.cache_spm.outputs.cache-hit }}
FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 5
FASTLANE_XCODEBUILD_SETTINGS_RETRIES: 10

- name: Deploy to TestFlight
run: bundle exec fastlane deploy changelog:${{ github.head_ref || github.ref_name }} --env $FASTLANE_ENV

- name: Create Git Tag
uses: actions/github-script@v7
with:
github-token: ${{ secrets.HOPPSEN_BOT_TOKEN }}
script: |
const tagName = `${process.env.VERSION_NUMBER}-${process.env.BUILD_NUMBER}`;
await github.rest.git.createRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `refs/tags/${tagName}`,
sha: context.sha
});

- name: Upload metadata to App Store Connect
run: bundle exec fastlane upload_metadata version_number:${{ env.VERSION_NUMBER }} build_number:${{ env.BUILD_NUMBER }} --env $FASTLANE_ENV

- name: Upload output files
uses: actions/upload-artifact@v4
with:
name: outputs
path: |
culprits.txt
fastlane/output/*.ipa
fastlane/output/*.dSYM.zip
retention-days: 14
5 changes: 4 additions & 1 deletion .swiftformat
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
# file options

--exclude Pods,**/.build,**/Package.swift,vendor/bundle,scripts,fastlane,**/L10n.swift,**/Assets.swift,**/*.generated.swift
--exclude Pods,**/.build,**/Package.swift,vendor/bundle,scripts,fastlane,**/L10n.swift,**/Assets.swift,**/*.generated.swift,**/Backport.swift

# format options
--swiftversion 5.5

## fileHeader
--header strip

## elseOnSameLine
--guardelse same-line

Expand Down
Loading
Loading