Category: CICD-Pipeline

CICD Pipeline is a topic the MagnusMinds engineering team writes about from project work: how it is set up, where it breaks, and what we changed in production. This category page lists all 3 articles filed under CICD Pipeline.

Start with “One Button, Two Stores: Shipping Your Flutter App to Play Store and App Store with GitHub Actions”, “Angular App Hosting on IIS: Quick Guide”, or “GitHub CI/CD Tutorial for Pipelines”. Open a post for the full write-up, or follow another tag or category on the card to a related topic.

One Button, Two Stores: Shipping Your Flutter App to Play Store and App Store with GitHub Actions
Oct 30, 2026 12 min read

One Button, Two Stores: Shipping Your Flutter App to Play Store and App Store with GitHub Actions and Fastlane How we went from “whose laptop has the keystore?” to a single Run workflow button that tests, builds, signs and uploads our Android and iOS apps, without anyone on the team owning a Mac. The problem We build a Flutter app for Android and iOS. Like most teams, we started out releasing it by hand, and every release was slow and a little risky. The biggest blocker was iOS. To build an iOS app and upload it to the App Store or TestFlight, you need Xcode, and Xcode only runs on a Mac. Every iOS release meant finding a Mac, installing a distribution certificate and a provisioning profile, and hoping none of them had expired since the last time. Android was easier to build but had its own problems: The upload keystore lived on one or two laptops. If that person was on leave, nothing shipped. Someone had to bump the build number in pubspec.yaml by hand. Sometimes two people uploaded the same number and Play rejected the second one. Every so often a “quick test build” went out pointing at the wrong API. We wanted releases to be boring: repeatable, one click, the same steps every time, with no secrets on anybody’s laptop and no Mac on anybody’s desk. What we ended up with A single manually-triggered GitHub Actions workflow with three inputs: Input Choices Meaning platform android, ios, both Which app(s) to release api dev, prod Which backend the build talks to version Text such as 0.1.5 (optional) The version name shown in the stores and the app; empty uses pubspec.yaml Anyone on the team presses Actions → Mobile release → Run workflow, from any computer. About 15 to 25 minutes later: Android: a signed App Bundle is released to Google Play’s Internal testing track, and testers can install it straight away. iOS: a signed build is processing in TestFlight, and internal testers get it automatically. Why GitHub Actions and Fastlane? We split the work between two tools, each doing what it’s best at. Fastlane handles the how: talking to Google Play and App Store Connect, working out version numbers, running the Flutter build, signing and uploading. It runs the same way on a laptop as it does on CI, so we can still debug a release locally. GitHub Actions handles the where and when: which machine runs the build (Linux for Android, macOS for iOS), which secrets are injected, who has to approve a production build, and keeping the built files for later. That split keeps the workflow file short and easy to read. iOS without a Mac and without Xcode This was the part we were most worried about, and it ended up being the cleanest. GitHub provides hosted macOS machines that already have Xcode installed. Our iOS job simply runs on one of them, so the Mac is GitHub’s problem, not ours. Signing was the other half. The traditional approach is to export a certificate (.p12) and a provisioning profile, store them as secrets, and install them on every run. That’s a lot of moving parts, and certificates expire every year. Instead we use automatic signing with an App Store Connect API key. The only iOS secrets are the API key itself and our Apple Team ID. During the build, Xcode uses that key to create or reuse the distribution certificate and App Store profile on the fly. There are no certificate files to store, renew or lose. Two small settings make iOS releases even smoother: The app’s Info.plist declares that it only uses standard encryption (HTTPS), so TestFlight doesn’t stop each upload to ask about export compliance. The workflow doesn’t wait for Apple to finish processing the build, which takes 10 to 30 minutes. There’s no reason to pay for a macOS machine to sit and watch that. Every release follows the same four stages Android and iOS run the same shape of pipeline, so once you understand one you understand both: Stage What happens Test flutter analyze and flutter test. Any failure stops everything; nothing is built or uploaded. Version Ask the store for the latest build number and add 1. The version name comes from the version input, or pubspec.yaml when it’s empty. Build A clean release build with the right version and API URL baked in. Upload Android goes to Play Internal testing. iOS goes to TestFlight. Key decisions The store decides the build number Keeping the build number in a file means someone has to remember to bump it, and two branches can bump it to the same value. So we stopped trusting the file. Before each build, Fastlane asks Google Play (or TestFlight) for the highest build number it has ever seen and adds one. Nothing is written back to the repo, so the workflow only ever needs read access. Type the version once, see it everywhere The optional version input sets the version name in one go. Type 0.1.5 and it shows up in Play Console, App Store Connect, TestFlight and on the app’s own profile screen. Only a strict format like 0.1.5 is accepted, so a typo fails in seconds instead of after a 20-minute build. Leave it empty and the version from pubspec.yaml is used. Release to testers directly, keep production human Android builds go straight to the Internal testing track instead of waiting as drafts, and iOS builds go straight to TestFlight. Only the app itself is uploaded; the store listing, screenshots and descriptions are never touched. Going live stays a human decision. Builds that use the production API pause until an approved reviewer clicks Approve in GitHub, and promoting a build to Production or submitting it for App Store review is still done by hand, after someone has tested it. Secrets you need Everything sensitive lives in Settings → Secrets and variables → Actions. Nothing is committed to the repo and nothing is needed on a developer’s machine. Secret Platform What it is ANDROID_KEYSTORE_BASE64 Android The upload keystore, base64-encoded ANDROID_KEYSTORE_PASSWORD Android Keystore password ANDROID_KEY_ALIAS Android Key alias ANDROID_KEY_PASSWORD Android Key password PLAY_STORE_JSON_KEY Android Google Cloud service-account JSON key APPLE_TEAM_ID iOS 10-character Apple Developer team ID ASC_KEY_ID iOS App Store Connect API Key ID ASC_ISSUER_ID iOS App Store Connect API Issuer ID ASC_KEY_P8_BASE64 iOS The AuthKey_XXXX.p8 file, base64-encoded The dev and production API URLs are stored as plain variables, since they aren’t secret. One-time store setup The workflow is the easy part. Most of the time goes into store-side setup, and you only do it once. Google Play Upload the first build by hand. The Play API can only upload to an app that already has at least one release. Enable the Google Play Android Developer API in any Google Cloud project. Create a service account and download a JSON key. Invite the service account’s email in Play Console under Users and permissions, and allow it to release to testing tracks. Wait about five minutes for the permissions to take effect. Don’t generate a new keystore: Play only accepts builds signed with the upload key it already knows. Apple Join the Apple Developer Program for your organisation. Create the app in App Store Connect with the right bundle ID. Create an App Store Connect API key with the Admin role, so it can create certificates and profiles. Download the .p8 file straight away, because Apple only lets you download it once. Create an internal TestFlight group and add your testers. The workflow file Save this as .github/workflows/mobile-release.yml: name: Mobile release on: workflow_dispatch: inputs: platform: description: App to release required: true type: choice options: [android, ios, both] default: android api: description: API the build talks to (prod waits for approval) required: true type: choice options: [dev, prod] default: dev version: description: Version shown in the stores, e.g. 0.1.5 (empty = pubspec.yaml) required: false type: string default: '' permissions: contents: read env: FLUTTER_VERSION: 3.47.7 RUBY_VERSION: '3.3' APP_VERSION_NAME: ${{ inputs.version }} jobs: test: name: Analyze and test runs-on: ubuntu-latest defaults: run: working-directory: apps/mobile steps: - uses: actions/checkout@v4 - name: Check the version input if: inputs.version != '' run: | if ! [[ "$APP_VERSION_NAME" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "::error::version must look like 0.1.5, got '$APP_VERSION_NAME'" exit 1 fi - uses: subosito/flutter-action@v2 with: channel: stable flutter-version: ${{ env.FLUTTER_VERSION }} cache: true - run: flutter pub get - run: flutter analyze - run: flutter test android: name: Android ? Play Internal testing needs: test if: inputs.platform != 'ios' runs-on: ubuntu-latest concurrency: group: mobile-release-android cancel-in-progress: false environment: ${{ inputs.api == 'prod' && 'production' || 'development' }} env: SKIP_FLUTTER_TESTS: 'true' APP_PLAY_RELEASE_STATUS: completed APP_DEV_API_URL: ${{ vars.APP_DEV_API_URL }} APP_PROD_API_URL: ${{ vars.APP_PROD_API_URL }} APP_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} APP_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} APP_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: distribution: temurin java-version: '17' - uses: subosito/flutter-action@v2 with: channel: stable flutter-version: ${{ env.FLUTTER_VERSION }} cache: true - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ env.RUBY_VERSION }} bundler-cache: true working-directory: apps/mobile - name: Restore the upload keystore and the Play key env: KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} PLAY_JSON: ${{ secrets.PLAY_STORE_JSON_KEY }} run: | echo "$KEYSTORE_BASE64" | base64 --decode > "$RUNNER_TEMP/upload-keystore.jks" printf '%s' "$PLAY_JSON" > "$RUNNER_TEMP/play-store.json" echo "APP_KEYSTORE_PATH=$RUNNER_TEMP/upload-keystore.jks" >> "$GITHUB_ENV" echo "APP_PLAY_STORE_JSON_KEY=$RUNNER_TEMP/play-store.json" >> "$GITHUB_ENV" - name: Build and release to Internal testing working-directory: apps/mobile/android run: bundle exec fastlane ${{ inputs.api == 'prod' && 'internal' || 'dev' }} - uses: actions/upload-artifact@v4 with: name: android-${{ inputs.api }}-${{ inputs.version && format('{0}-', inputs.version) || '' }}${{ github.run_number }} path: apps/mobile/build/app/outputs/bundle/release/app-release.aab retention-days: 14 - name: Remove the keystore and key if: always() run: rm -f "$RUNNER_TEMP/upload-keystore.jks" "$RUNNER_TEMP/play-store.json" ios: name: iOS ? TestFlight needs: test if: inputs.platform != 'android' runs-on: macos-latest concurrency: group: mobile-release-ios cancel-in-progress: false environment: ${{ inputs.api == 'prod' && 'production' || 'development' }} env: SKIP_FLUTTER_TESTS: 'true' APP_DEV_API_URL: ${{ vars.APP_DEV_API_URL }} APP_PROD_API_URL: ${{ vars.APP_PROD_API_URL }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }} ASC_KEY_P8_BASE64: ${{ secrets.ASC_KEY_P8_BASE64 }} steps: - uses: actions/checkout@v4 - uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: latest-stable - uses: subosito/flutter-action@v2 with: channel: stable flutter-version: ${{ env.FLUTTER_VERSION }} cache: true - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ env.RUBY_VERSION }} bundler-cache: true working-directory: apps/mobile - name: Build, sign and upload to TestFlight working-directory: apps/mobile/ios run: bundle exec fastlane ${{ inputs.api }} - uses: actions/upload-artifact@v4 with: name: ios-${{ inputs.api }}-${{ inputs.version && format('{0}-', inputs.version) || '' }}${{ github.run_number }} path: apps/mobile/build/ios/ipa/App.ipa retention-days: 14 How the workflow works, in short Inputs: when you click Run workflow, you choose the platform (Android, iOS or both), the API (dev or prod) and, optionally, the version number. permissions: the workflow can only read the repo. It never commits anything back. test job: checks the version format, then runs flutter analyze and flutter test once on Linux. If anything fails, the build jobs never start. The build jobs set SKIP_FLUTTER_TESTS so the tests don’t run again on the more expensive machines. android job: runs on Linux. It decodes the keystore and Play key from secrets into a temporary folder, then Fastlane builds, signs and releases the app to Play Internal testing. The last step deletes the keystore and key, even if the build failed. ios job: runs on GitHub’s macOS machine with the latest stable Xcode. Fastlane builds the app, signs it using the App Store Connect API key and uploads it to TestFlight. This is the job that replaces your own Mac. environment: builds using the prod API run in the production environment, which waits for an approved reviewer. Dev builds run straight away. concurrency: only one upload per store runs at a time. A second run waits its turn, so two releases can never pick the same build number. upload-artifact: keeps a copy of the .aab and .ipa for 14 days, named with the API, version and run number. Troubleshooting These are the errors we actually hit, and what fixed them: Message Fix The caller does not have permission Invite the service account in Play Console and grant app permissions, then wait about 5 minutes. Could not read any versionCode from Play The service account can’t see the app, or no build was ever uploaded by hand. Version code X has already been used Two uploads raced. The concurrency group prevents this on CI; just rerun. The Android App Bundle was signed with the wrong key The keystore secret isn’t the upload key Play expects. iOS export fails after a successful archive Put the signing settings in the Xcode project instead of passing them as build arguments. What we got out of it Anyone on the team can ship, from Windows, Mac or Linux, with nothing installed locally. No Mac and no Xcode needed for iOS releases, and no certificate files to renew. No secrets on laptops. Keys exist only as encrypted GitHub secrets and on a build machine for the length of a job. No more build number clashes, because the store decides and uploads run one at a time. Tests always gate a release. A red test means nothing is built. People still make the go-live decision for production. If you’re running a Flutter app with a small team and still releasing from someone’s laptop, this setup takes about a day, and most of that day is store-console setup you only do once. After that, every release is one button. Stack: Flutter 3.47, Fastlane 2.228, Ruby 3.3, GitHub Actions (ubuntu-latest and macos-latest).

Angular App Hosting on IIS: Quick Guide
Jan 23, 2024 2 min read

Hosting an Angular application on IIS involves a few straightforward steps.    Follow this step-by-step guide to seamlessly deploy your Angular project on IIS. Step 1: Open Your Angular Project in Visual Studio Code Review the build command in the package.json file. By default, it's usually set to ng build. Step 2: Run the Build Command Execute the ng build command in the terminal to compile your Angular application.  This command creates a 'dist' folder, typically located at the specified output path in the angular.json file. Step 3: Install IIS Ensure that IIS is installed on your machine. You can install it through the "Turn Windows features on or off" option in the Control Panel. Step 4: Create a New Site in IIS Open the IIS Manager. In the Connections pane, right-click on the "Sites" node and select "Add Website." Fill in the required information, such as the Site name, Physical path to the folder , and choose a port. Step 5: Configure URL Rewrite (Optional) If your Angular application uses routing, consider configuring URL Rewrite for proper routing.  Create a 'web.config' file in your 'dist' folder with the appropriate configurations. Here's a simple example of a web.config file for an Angular application with routing.This file helps configure how the server handles URL requests. ---------------------------------------------------------------------------------------------------------------- <?xml version="1.0" encoding="utf-8"?> <configuration>   <system.webServer>     <rewrite>       <rules>         <rule name="Angular Routes" stopProcessing="true">           <match url=".*" />           <conditions logicalGrouping="MatchAll">             <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />             <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />           </conditions>           <action type="Rewrite" url="/" />         </rule>       </rules>     </rewrite>     <staticContent>       <remove fileExtension=".json" />       <mimeMap fileExtension=".json" mimeType="application/json" />     </staticContent>   </system.webServer> </configuration> ---------------------------------------------------------------------------------------------------------------- Step 6: Restart IIS After making these changes, restart IIS to apply the configurations. Step 7: Access Your Angular Application Open a web browser and navigate to http://localhost:yourport (replace 'yourport' with the specified port from Step 4). Now, your Angular application is hosted on IIS. Access it through the specified port. If any issues arise, check the IIS logs for more information.  Customize these instructions based on your specific requirements and environment. Thanks!

GitHub CI/CD Tutorial for Pipelines
Jan 12, 2024 6 min read

In this blog, I will guide you on the power of CI/CD in GitHub with a step-by-step guide. Learn to set up automated workflows, boost project efficiency, and streamline development processes for better code quality and faster deployments. Certainly! It seems that I've encountered a challenge in my current development workflow when deploying minor code changes. The existing process involves manually publishing code from Visual Studio, creating backups of the current code on the server, and then replacing it with the new code. To address this, it's advisable to transition to an automated solution utilizing a Continuous Integration/Continuous Deployment (CI/CD) pipeline.  By implementing a CI/CD pipeline, you can streamline and automate the deployment process, making it more efficient and reducing the risk of manual errors. The CI/CD pipeline will handle tasks such as code compilation, testing, and deployment automatically, ensuring that the latest changes are seamlessly deployed to the desired environment.  This transition will not only save time but also enhance the reliability of your deployment process, allowing your team to focus more on development and less on manual deployment tasks.  For additional information, refer to the steps outlined below for guidance.   Step 1:  Go to your repository and click on the Actions tab   Step 2:  Now, Select the workflow according to your development. Here I am using .NET workflow.   Step 3:  Now you can see the default pipeline as below. In that, you can change your branch as per your requirement. Step 4:  You can now incorporate three new sections as outlined below to build the code and publish the folder as an artifact.  - name: Build and publish      run: |        dotnet restore        dotnet build        dotnet publish -o publish    - name: Zip output      run: |        cd publish        zip -r ../output .  - name: Upload zip archive      uses: actions/upload-artifact@v2      with:        name: test        path: ./publish  Upon integrating this code, your YAML file will now appear as follows.  In the code above, you have the flexibility to rename the zip file or the publish folder according to your preferences.  Build and Publish : This step is responsible for building and publishing the code.  Commands:  dotnet restore: Restores the project's dependencies.  dotnet build: Compiles the project.  dotnet publish -o publish: Publishes the project output to the 'publish' folder.    Zip Output : This step involves compressing the contents of the 'publish' folder into a zip file.  Commands:  cd publish: Changes the working directory to the 'publish' folder.  zip -r ../output .: Creates a zip file named 'output' containing the contents of the 'publish' folder.    Upload Zip Archive :This step uploads the zip archive to the workflow run as an artifact.  Using: The actions/upload-artifact@v2 GitHub Action.  Configuration:  name: test: Specifies the name of the artifact as 'test'.  path: ./publish: Indicates the path of the folder to be archived and uploaded.  By using the given code, you receive a finalized published folder prepared for deployment on the server. However, the deployment process on the server requires manual intervention.  To access the published folder, navigate to the "Actions" tab. Click on the "test" workflow, and you can download the published folder from there.  Step 5:  In the steps mentioned above, you previously followed a manual process, but now you have transitioned to an automatic process.  To automate the process, you'll need to install a self-hosted runner on the virtual machine where your application is hosted.  What is Self-hosted runner?  self-hosted runner is a system that you deploy and manage to execute jobs from GitHub Actions on GitHub.com.  To install the self-hosted runner, follow the basic steps.  Under your repository name, click Settings. If you cannot see the "Settings" tab, select the dropdown menu, then click Settings. In the left sidebar, click Actions, then click Runners and then click on New self-hosted runner.  Select the operating system image and architecture of your self-hosted runner machine.  Open a shell on your self-hosted runner machine and run each shell command in the order shown. For more details you can visit https://docs.github.com/en/enterprise-cloud@latest/actions/hosting-your-own-runners/managing-self-hosted-runners/adding-self-hosted-runners  Step 6:  To automate the process, you can remove the last two sections, "Zip Output" and "Upload Zip Archive," and replace them with the following code.  - name: Backup & Deploy      run: |        $datestamp = Get-Date -Format "yyyyMMddHHmmss"        cd publish        Remove-Item web.config        Remove-Item appsettings.json        Remove-Item appsettings.Development.json        Stop-WebSite 'DemoGitHubPipeline'        Compress-Archive D:\Published\DemoGitHubPipeline         D:\Published\Backup\Backup_$datestamp.zip        Copy-Item * D:\Published\DemoGitHubPipeline -Recurse -Force        Start-WebSite 'DemoGitHubPipeline'  Backup & Deploy : This step is responsible for creating a backup, making necessary modifications, and deploying the application. Commands:  $datestamp = Get-Date -Format "yyyyMMddHHmmss": Retrieves the current date and time in the specified format.  cd publish: Changes the working directory to the 'publish' folder.  Remove-Item web.config: Deletes the 'web.config' file.  Remove-Item appsettings.json: Deletes the 'appsettings.json' file.  Remove-Item appsettings.Development.json: Deletes the 'appsettings.Development.json' file.  Stop-WebSite 'DemoGitHubPipeline': Stops the website with the specified name.  Compress-Archive D:\Published\DemoGitHubPipeline D:\Published\Backup\Backup_$datestamp.zip: Creates a compressed archive (zip) of the existing deployment with proper timestamp.  Copy-Item * D:\Published\DemoGitHubPipeline -Recurse -Force: Copies all contents from the 'publish' folder to the deployment directory.  Start-WebSite 'DemoGitHubPipeline': Restarts the website with the specified name.    Note:  Ensure that the paths and folder structures match the actual locations in your setup.  Adjust the website name and paths based on your specific configuration.  Conclusion: In summary, implementing a CI/CD pipeline in GitHub is a pivotal step towards achieving efficiency, reliability, and accelerated development cycles. The integration of CI/CD streamlines the software delivery process by automating testing, building, and deployment, leading to consistent and high-quality releases.  GitHub Actions, with its native CI/CD capabilities, provides a powerful and flexible platform for orchestrating workflows. Leveraging its features, development teams can not only automate repetitive tasks but also ensure rapid feedback on code changes, enabling early detection of issues and facilitating collaboration.