Skip to content

How To: CI/CD Integration

Automate PyLocket protection in your CI/CD pipeline to protect every release automatically.


Authentication in CI/CD

Use an API token instead of interactive login. Generate one in the Developer Portal or CLI:

pylocket auth api-keys rotate

Store the token as a CI/CD secret (e.g., PYLOCKET_TOKEN).

Use it in commands:

export PYLOCKET_TOKEN=<your-token>
pylocket protect --app <APP_ID> --artifact dist/myapp.exe ...

Or pass it explicitly:

pylocket protect --token <your-token> --app <APP_ID> --artifact dist/myapp.exe ...

GitHub Actions

name: Build, Protect, Release

on:
  push:
    tags: ["v*"]

jobs:
  protect:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        include:
          - platform: linux-x64
            artifact: dist/myapp
            os_label: linux
          - platform: win-x64
            artifact: dist/myapp.exe
            os_label: windows

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install dependencies
        run: |
          pip install pylocket pyinstaller
          pip install -r requirements.txt

      - name: Build
        run: pyinstaller --onefile myapp.py

      - name: Protect
        env:
          PYLOCKET_TOKEN: ${{ secrets.PYLOCKET_TOKEN }}
        run: |
          pylocket protect \
            --app ${{ vars.PYLOCKET_APP_ID }} \
            --artifact ${{ matrix.artifact }} \
            --platform ${{ matrix.platform }} \
            --python 3.12

      - name: Wait for protection
        env:
          PYLOCKET_TOKEN: ${{ secrets.PYLOCKET_TOKEN }}
        run: |
          # Get the latest build ID
          BUILD_ID=$(pylocket status \
            --app ${{ vars.PYLOCKET_APP_ID }} \
            --latest --format json | jq -r '.build_id')

          # Poll until ready
          for i in $(seq 1 60); do
            STATUS=$(pylocket status --build "$BUILD_ID" --format json | jq -r '.status')
            echo "Build status: $STATUS"
            if [ "$STATUS" = "READY" ]; then
              echo "BUILD_ID=$BUILD_ID" >> "$GITHUB_ENV"
              exit 0
            fi
            if [ "$STATUS" = "FAILED" ]; then
              echo "::error::Protection failed"
              exit 1
            fi
            sleep 10
          done
          echo "::error::Timed out waiting for protection"
          exit 1

      - name: Download protected artifact
        env:
          PYLOCKET_TOKEN: ${{ secrets.PYLOCKET_TOKEN }}
        run: pylocket fetch --build "$BUILD_ID" --out dist/protected/

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: myapp-${{ matrix.os_label }}
          path: dist/protected/

GitLab CI

# .gitlab-ci.yml
stages:
  - build
  - protect
  - release

variables:
  PYTHON_VERSION: "3.12"

build:
  stage: build
  image: python:3.12
  script:
    - pip install pyinstaller -r requirements.txt
    - pyinstaller --onefile myapp.py
  artifacts:
    paths:
      - dist/

protect:
  stage: protect
  image: python:3.12
  script:
    - pip install pylocket
    - export PYLOCKET_TOKEN=$PYLOCKET_TOKEN
    - pylocket protect
        --app $PYLOCKET_APP_ID
        --artifact dist/myapp
        --platform linux-x64
        --python $PYTHON_VERSION
    - |
      BUILD_ID=$(pylocket status --app $PYLOCKET_APP_ID --latest --format json | jq -r '.build_id')
      while true; do
        STATUS=$(pylocket status --build "$BUILD_ID" --format json | jq -r '.status')
        [ "$STATUS" = "READY" ] && break
        [ "$STATUS" = "FAILED" ] && exit 1
        sleep 10
      done
    - pylocket fetch --build "$BUILD_ID" --out dist/protected/
  artifacts:
    paths:
      - dist/protected/

Jenkins

// Jenkinsfile
pipeline {
    agent any

    environment {
        PYLOCKET_TOKEN = credentials('pylocket-token')
        PYLOCKET_APP_ID = 'app_abc123'
    }

    stages {
        stage('Build') {
            steps {
                sh 'pip install pyinstaller -r requirements.txt'
                sh 'pyinstaller --onefile myapp.py'
            }
        }

        stage('Protect') {
            steps {
                sh '''
                    pip install pylocket
                    pylocket protect \
                      --app $PYLOCKET_APP_ID \
                      --artifact dist/myapp \
                      --platform linux-x64 \
                      --python 3.12
                '''

                script {
                    def buildId = sh(
                        script: 'pylocket status --app $PYLOCKET_APP_ID --latest --format json | jq -r .build_id',
                        returnStdout: true
                    ).trim()

                    timeout(time: 10, unit: 'MINUTES') {
                        waitUntil {
                            def status = sh(
                                script: "pylocket status --build ${buildId} --format json | jq -r .status",
                                returnStdout: true
                            ).trim()
                            return status == 'READY'
                        }
                    }

                    sh "pylocket fetch --build ${buildId} --out dist/protected/"
                }
            }
        }
    }

    post {
        success {
            archiveArtifacts artifacts: 'dist/protected/**'
        }
    }
}

Best Practices

Practice Rationale
Store PYLOCKET_TOKEN as a secret Never hardcode tokens in pipeline files
Store PYLOCKET_APP_ID as a variable Makes it easy to change without editing the pipeline
Set a timeout on the status polling loop Prevents infinite waits on failed builds
Pin the pylocket CLI version Avoid unexpected behavior from CLI updates: pip install pylocket==1.0.0

See Also