Skip to content
Latchkey

Build workflow (floccusaddon/floccus)

The Build workflow from floccusaddon/floccus, explained and optimized by Latchkey.

F

CI health: F - at risk

Point runs-on at Latchkey and get caching, run de-duplication, job timeouts, SHA-pinned actions, self-healing for flaky steps, and up to 58% lower cost, applied automatically.

Grade your own workflow free or run it on Latchkey →
Source: floccusaddon/floccus.github/workflows/build.ymlLicense MPL-2.0View source

What it does

This is the Build workflow from the floccusaddon/floccus repository, a real project running GitHub Actions. It is shown here with attribution under its MPL-2.0 license.

Below, Latchkey shows a faster, safer version produced by its optimization engine.

The workflow

workflow (.yml)
name: Build

on:
  pull_request:
  push:
    branches:
      - develop
      - master

jobs:
  js:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [20.x]
        npm-version: [10.x]

    name: js node${{ matrix.node-version }}
    steps:
      - uses: actions/checkout@v2

      - name: Set up node ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}

      - name: Set up npm ${{ matrix.npm-version }}
        run: npm i -g npm@"${{ matrix.npm-version }}"

      - name: Cache node modules
        uses: actions/cache@v4
        env:
          cache-name: cache-node-modules
        with:
          path: ~/.npm # npm cache files are stored in `~/.npm` on Linux/macOS
          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-build-${{ env.cache-name }}-
            ${{ runner.os }}-build-
            ${{ runner.os }}-

      - name: Install dependencies
        run: |
          npm ci -f

      - name: Lint code
        run: |
          npm run lint

      - name: TypeScript check
        run: |
          npm run typecheck

      - name: Install dependencies & build
        run: |
          npm run build-release --if-present

      - name: Static check if sentry lazy loading is absent
        run: |
          grep -qvrni '"https://browser.sentry-cdn.com"' ./dist/js/ || (echo 'Problem: Build includes lazy-loading of components from sentry-cdn.com. This will get rejected by Chrome Webstore review!' && exit 1)

      - name: Static check if css is compatible with the required browsers
        run: |
          npm run doiuse | sed 's/[^:]*:*//' | sed 's/> *//' | LC_ALL=C sort > doiuse-report.txt
          cat doiuse-report.txt
          diff doiuse-report.txt doiuse-report.baseline.txt || (echo 'Problem: Build includes CSS that is not compatible with the required browsers. Please check the report above!' && exit 1)

      - name: Save context
        uses: actions/cache/save@v4
        with:
          key: build-context-${{ github.run_id }}
          path: ./

  android:
    needs: js
    runs-on: ubuntu-latest
    strategy:
      matrix:
        java-version: [ 21 ]
    name: android java${{ matrix.java-version }}
    steps:
      - uses: actions/setup-java@v4
        with:
          distribution: 'oracle'
          java-version: ${{ matrix.java-version }}

      - name: Restore context
        uses: actions/cache/restore@v4
        with:
          fail-on-cache-miss: true
          key: build-context-${{ github.run_id }}
          path: ./

      - name: Create JKS file from secrets
        if: github.ref_name == 'develop' || github.ref_name == 'master'
        env:
          ANDROID_CERT: ${{ secrets.ANDROID_CERT }}
          ANDROID_PRIV_KEY: ${{ secrets.ANDROID_PRIV_KEY }}
          ANDROID_JKS_PASSWORD: ${{ secrets.ANDROID_JKS_PASSWORD }}
        run: |
          # Create temporary directory
          mkdir -p keystore
          
          # Combine certificate and private key into PEM format
          echo "$ANDROID_CERT" > keystore/cert.pem
          echo "$ANDROID_PRIV_KEY" > keystore/key.pem
          
          # Combine certificate and private key into PKCS12 format
          # Use a temporary password since we need to specify one
          openssl pkcs12 -export -in keystore/cert.pem -inkey keystore/key.pem \
            -out keystore/app.p12 -name key0 -password pass:temp-password
          
          # Convert PKCS12 to JKS format
          keytool -importkeystore -srckeystore keystore/app.p12 -srcstoretype PKCS12 \
            -destkeystore keystore/app.jks -deststoretype JKS -srcstorepass temp-password \
            -deststorepass "$ANDROID_JKS_PASSWORD" -destkeypass "$ANDROID_JKS_PASSWORD"
          
          # Clean up temporary files
          rm keystore/cert.pem keystore/key.pem keystore/app.p12
          
          # Make sure the keystore file is not readable by others
          chmod 600 keystore/app.jks
          
      - name: Create gradle.properties file
        if: github.ref_name == 'develop' || github.ref_name == 'master'
        run: |
          cat >> android/gradle.properties << EOF
          FLOCCUS_STORE_FILE=../../keystore/app.jks
          FLOCCUS_STORE_PASSWORD=${{ secrets.ANDROID_JKS_PASSWORD }}
          FLOCCUS_KEY_ALIAS=key0
          FLOCCUS_KEY_PASSWORD=${{ secrets.ANDROID_JKS_PASSWORD }}
          EOF

      - name: Prepare build
        run: |
          npx cap sync
          cd android
          chmod +x gradlew

      - name: Build android release
        if: github.ref_name == 'develop' || github.ref_name == 'master'
        run: |
          cd android
          ./gradlew assembleRelease

      - name: Build android
        if: github.ref_name != 'develop' && github.ref_name != 'master'
        run: |
          cd android
          ./gradlew assemble

      - name: Upload build artifact
        if: github.ref_name == 'develop' || github.ref_name == 'master'
        uses: actions/upload-artifact@v4
        with:
          name: "floccus-build-${{ github.sha }}.apk"
          path: android/app/build/outputs/apk/release/app-release.apk
          retention-days: 7

  ios:
    needs: js
    runs-on: macos-latest
    name: ios
    steps:
      - name: Restore context
        uses: buildjet/cache/restore@v3
        with:
          fail-on-cache-miss: true
          key: build-context-${{ github.run_id }}
          path: ./

      - name: setup-cocoapods
        uses: maxim-lobanov/setup-cocoapods@v1
        with:
          podfile-path: ios/App/Podfile.lock

      - uses: maxim-lobanov/setup-xcode@v1
        with:
          xcode-version: latest-stable

      - name: Install certificate and provisioning profile
        env:
          BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
          BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }}
          BUILD_PROVISION_PROFILE_NEW_BOOKMARK_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_NEW_BOOKMARK_BASE64 }}
          KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
        run: |
          # create variables
          CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
          PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
          PP2_PATH=$RUNNER_TEMP/build_pp2.mobileprovision
          KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db

          # import certificate and provisioning profile from secrets
          echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
          echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH
          echo -n "$BUILD_PROVISION_PROFILE_NEW_BOOKMARK_BASE64" | base64 --decode -o $PP2_PATH

          # create temporary keychain
          security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
          security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
          security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH

          # import certificate to keychain
          security import $CERTIFICATE_PATH -P "" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
          security list-keychain -d user -s $KEYCHAIN_PATH

          # apply provisioning profile
          mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
          cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
          cp $PP2_PATH ~/Library/MobileDevice/Provisioning\ Profiles

      - name: Capacitor sync
        run: |
          npx cap sync

      - name: Build ios
        env:
          scheme: "Floccus"
        run: |
          cd ios/App
          xcodebuild build-for-testing -scheme "$scheme" -workspace App.xcworkspace

  summary:
    runs-on: ubuntu-latest
    needs: [ js, android, ios ]

    if: always()

    name: build-summary

    steps:
      - name: Summary status
        run: if ${{ needs.js.result != 'success' || ( needs.android.result != 'success' && needs.selenium.result != 'skipped' ) || ( needs.ios.result != 'success' && needs.ios.result != 'skipped' ) }}; then exit 1; fi

The same workflow, on Latchkey

Estimated ~20% faster on cache hits, plus fewer wasted runs and a safer supply chain. Added and changed lines are highlighted.

name: Build
 
on:
  pull_request:
  push:
    branches:
      - develop
      - master
 
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
 
jobs:
  js:
    timeout-minutes: 30
    runs-on: latchkey-small
 
    strategy:
      matrix:
        node-version: [20.x]
        npm-version: [10.x]
 
    name: js node${{ matrix.node-version }}
    steps:
      - uses: actions/checkout@v2
 
      - name: Set up node ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          cache: 'npm'
          node-version: ${{ matrix.node-version }}
 
      - name: Set up npm ${{ matrix.npm-version }}
        run: npm i -g npm@"${{ matrix.npm-version }}"
 
      - name: Cache node modules
        uses: actions/cache@v4
        env:
          cache-name: cache-node-modules
        with:
          path: ~/.npm # npm cache files are stored in `~/.npm` on Linux/macOS
          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-build-${{ env.cache-name }}-
            ${{ runner.os }}-build-
            ${{ runner.os }}-
 
      - name: Install dependencies
        run: |
          npm ci -f
 
      - name: Lint code
        run: |
          npm run lint
 
      - name: TypeScript check
        run: |
          npm run typecheck
 
      - name: Install dependencies & build
        run: |
          npm run build-release --if-present
 
      - name: Static check if sentry lazy loading is absent
        run: |
          grep -qvrni '"https://browser.sentry-cdn.com"' ./dist/js/ || (echo 'Problem: Build includes lazy-loading of components from sentry-cdn.com. This will get rejected by Chrome Webstore review!' && exit 1)
 
      - name: Static check if css is compatible with the required browsers
        run: |
          npm run doiuse | sed 's/[^:]*:*//' | sed 's/> *//' | LC_ALL=C sort > doiuse-report.txt
          cat doiuse-report.txt
          diff doiuse-report.txt doiuse-report.baseline.txt || (echo 'Problem: Build includes CSS that is not compatible with the required browsers. Please check the report above!' && exit 1)
 
      - name: Save context
        uses: actions/cache/save@v4
        with:
          key: build-context-${{ github.run_id }}
          path: ./
 
  android:
    timeout-minutes: 30
    needs: js
    runs-on: latchkey-small
    strategy:
      matrix:
        java-version: [ 21 ]
    name: android java${{ matrix.java-version }}
    steps:
      - uses: actions/setup-java@v4
        with:
          distribution: 'oracle'
          java-version: ${{ matrix.java-version }}
 
      - name: Restore context
        uses: actions/cache/restore@v4
        with:
          fail-on-cache-miss: true
          key: build-context-${{ github.run_id }}
          path: ./
 
      - name: Create JKS file from secrets
        if: github.ref_name == 'develop' || github.ref_name == 'master'
        env:
          ANDROID_CERT: ${{ secrets.ANDROID_CERT }}
          ANDROID_PRIV_KEY: ${{ secrets.ANDROID_PRIV_KEY }}
          ANDROID_JKS_PASSWORD: ${{ secrets.ANDROID_JKS_PASSWORD }}
        run: |
          # Create temporary directory
          mkdir -p keystore
          
          # Combine certificate and private key into PEM format
          echo "$ANDROID_CERT" > keystore/cert.pem
          echo "$ANDROID_PRIV_KEY" > keystore/key.pem
          
          # Combine certificate and private key into PKCS12 format
          # Use a temporary password since we need to specify one
          openssl pkcs12 -export -in keystore/cert.pem -inkey keystore/key.pem \
            -out keystore/app.p12 -name key0 -password pass:temp-password
          
          # Convert PKCS12 to JKS format
          keytool -importkeystore -srckeystore keystore/app.p12 -srcstoretype PKCS12 \
            -destkeystore keystore/app.jks -deststoretype JKS -srcstorepass temp-password \
            -deststorepass "$ANDROID_JKS_PASSWORD" -destkeypass "$ANDROID_JKS_PASSWORD"
          
          # Clean up temporary files
          rm keystore/cert.pem keystore/key.pem keystore/app.p12
          
          # Make sure the keystore file is not readable by others
          chmod 600 keystore/app.jks
          
      - name: Create gradle.properties file
        if: github.ref_name == 'develop' || github.ref_name == 'master'
        run: |
          cat >> android/gradle.properties << EOF
          FLOCCUS_STORE_FILE=../../keystore/app.jks
          FLOCCUS_STORE_PASSWORD=${{ secrets.ANDROID_JKS_PASSWORD }}
          FLOCCUS_KEY_ALIAS=key0
          FLOCCUS_KEY_PASSWORD=${{ secrets.ANDROID_JKS_PASSWORD }}
          EOF
 
      - name: Prepare build
        run: |
          npx cap sync
          cd android
          chmod +x gradlew
 
      - name: Build android release
        if: github.ref_name == 'develop' || github.ref_name == 'master'
        run: |
          cd android
          ./gradlew assembleRelease
 
      - name: Build android
        if: github.ref_name != 'develop' && github.ref_name != 'master'
        run: |
          cd android
          ./gradlew assemble
 
      - name: Upload build artifact
        if: github.ref_name == 'develop' || github.ref_name == 'master'
        uses: actions/upload-artifact@v4
        with:
          name: "floccus-build-${{ github.sha }}.apk"
          path: android/app/build/outputs/apk/release/app-release.apk
          retention-days: 7
 
  ios:
    timeout-minutes: 30
    needs: js
    runs-on: macos-latest
    name: ios
    steps:
      - name: Restore context
        uses: buildjet/cache/restore@v3
        with:
          fail-on-cache-miss: true
          key: build-context-${{ github.run_id }}
          path: ./
 
      - name: setup-cocoapods
        uses: maxim-lobanov/setup-cocoapods@v1
        with:
          podfile-path: ios/App/Podfile.lock
 
      - uses: maxim-lobanov/setup-xcode@v1
        with:
          xcode-version: latest-stable
 
      - name: Install certificate and provisioning profile
        env:
          BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
          BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }}
          BUILD_PROVISION_PROFILE_NEW_BOOKMARK_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_NEW_BOOKMARK_BASE64 }}
          KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
        run: |
          # create variables
          CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
          PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
          PP2_PATH=$RUNNER_TEMP/build_pp2.mobileprovision
          KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
 
          # import certificate and provisioning profile from secrets
          echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
          echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH
          echo -n "$BUILD_PROVISION_PROFILE_NEW_BOOKMARK_BASE64" | base64 --decode -o $PP2_PATH
 
          # create temporary keychain
          security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
          security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
          security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
 
          # import certificate to keychain
          security import $CERTIFICATE_PATH -P "" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
          security list-keychain -d user -s $KEYCHAIN_PATH
 
          # apply provisioning profile
          mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
          cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
          cp $PP2_PATH ~/Library/MobileDevice/Provisioning\ Profiles
 
      - name: Capacitor sync
        run: |
          npx cap sync
 
      - name: Build ios
        env:
          scheme: "Floccus"
        run: |
          cd ios/App
          xcodebuild build-for-testing -scheme "$scheme" -workspace App.xcworkspace
 
  summary:
    timeout-minutes: 30
    runs-on: latchkey-small
    needs: [ js, android, ios ]
 
    if: always()
 
    name: build-summary
 
    steps:
      - name: Summary status
        run: if ${{ needs.js.result != 'success' || ( needs.android.result != 'success' && needs.selenium.result != 'skipped' ) || ( needs.ios.result != 'success' && needs.ios.result != 'skipped' ) }}; then exit 1; fi
 

What changed

3 third-party actions are referenced by a movable tag. Pin them to the commit SHA (Latchkey resolves and applies this automatically) so a repointed tag cannot change what runs.

What Latchkey heals here

This workflow has steps that commonly fail on transient issues (network, registries, flaky browsers). On Latchkey managed runners they are detected, retried, and self-healed instead of failing your build:

This workflow runs 4 jobs per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.

Actions used in this workflow