Skip to content

Long Workflows

Job Dependency Strategy

Break long workflows into separate jobs with dependencies. Each job can regenerate a fresh token, ensuring operations never fail due to expiration.

Long-Running Workflow Patterns

Pattern 1: Multi-Phase Workflow with Token Refresh

name: Multi-Phase Long-Running Workflow

on:
  workflow_dispatch:

jobs:
  phase-1:
    runs-on: ubuntu-latest
    outputs:
      token: ${{ steps.token.outputs.token }}
    steps:
      - name: Phase 1 token
        id: token
        uses: actions/create-github-app-token@v2
        with:
          app-id: ${{ secrets.CORE_APP_ID }}
          private-key: ${{ secrets.CORE_APP_PRIVATE_KEY }}
          owner: adaptive-enforcement-lab

      - name: Phase 1 operations (0-50 minutes)
        env:
          GH_TOKEN: ${{ steps.token.outputs.token }}
        run: |
          echo "Starting phase 1: $(date)"
          # Simulate long-running operations
          for i in {1..50}; do
            gh api user --jq .login
            sleep 60
          done
          echo "Completed phase 1: $(date)"

  phase-2:
    needs: phase-1
    runs-on: ubuntu-latest
    outputs:
      token: ${{ steps.token.outputs.token }}
    steps:
      - name: Phase 2 token (refreshed)
        id: token
        uses: actions/create-github-app-token@v2
        with:
          app-id: ${{ secrets.CORE_APP_ID }}
          private-key: ${{ secrets.CORE_APP_PRIVATE_KEY }}
          owner: adaptive-enforcement-lab

      - name: Phase 2 operations (50-100 minutes)
        env:
          GH_TOKEN: ${{ steps.token.outputs.token }}
        run: |
          echo "Starting phase 2: $(date)"
          for i in {1..50}; do
            gh api user --jq .login
            sleep 60
          done
          echo "Completed phase 2: $(date)"

  phase-3:
    needs: phase-2
    runs-on: ubuntu-latest
    steps:
      - name: Phase 3 token (refreshed again)
        id: token
        uses: actions/create-github-app-token@v2
        with:
          app-id: ${{ secrets.CORE_APP_ID }}
          private-key: ${{ secrets.CORE_APP_PRIVATE_KEY }}
          owner: adaptive-enforcement-lab

      - name: Phase 3 operations (100-150 minutes)
        env:
          GH_TOKEN: ${{ steps.token.outputs.token }}
        run: |
          echo "Starting phase 3: $(date)"
          for i in {1..50}; do
            gh api user --jq .login
            sleep 60
          done
          echo "Completed phase 3: $(date)"

Pattern 2: Continuous Operation with Error Recovery

name: Continuous Operation with Token Refresh

on:
  workflow_dispatch:

jobs:
  continuous-operation:
    runs-on: ubuntu-latest
    steps:
      - name: Setup token refresh function
        run: |
          cat > /tmp/refresh_token.sh << 'EOF'
          #!/bin/bash
          # Function to refresh token when needed
          refresh_token_if_needed() {
            local token_created=$1
            local current_time=$(date +%s)
            local token_age=$((current_time - token_created))
            local max_age=3300  # 55 minutes

            if [ $token_age -gt $max_age ]; then
              echo "true"
            else
              echo "false"
            fi
          }
          export -f refresh_token_if_needed
          EOF
          chmod +x /tmp/refresh_token.sh

      - name: Long-running operation with automatic refresh
        env:
          APP_ID: ${{ secrets.CORE_APP_ID }}
          PRIVATE_KEY: ${{ secrets.CORE_APP_PRIVATE_KEY }}
        run: |
          source /tmp/refresh_token.sh

          # Initial token generation
          export GH_TOKEN=$(gh api /app/installations \
            --jq '.[0].id' | xargs -I {} \
            gh api /app/installations/{}/access_tokens \
            -X POST --jq .token)
          TOKEN_CREATED=$(date +%s)

          # Continuous operation with error recovery
          ITERATION=1
          MAX_ITERATIONS=200  # ~3+ hours

          while [ $ITERATION -le $MAX_ITERATIONS ]; do
            # Check if token refresh needed
            if [ "$(refresh_token_if_needed $TOKEN_CREATED)" = "true" ]; then
              echo "::notice::Refreshing token (iteration $ITERATION)"
              export GH_TOKEN=$(gh api /app/installations \
                --jq '.[0].id' | xargs -I {} \
                gh api /app/installations/{}/access_tokens \
                -X POST --jq .token)
              TOKEN_CREATED=$(date +%s)
            fi

            # Perform operation with error recovery
            if ! gh api user --jq .login > /dev/null 2>&1; then
              echo "::warning::API call failed (iteration $ITERATION) - refreshing token"
              export GH_TOKEN=$(gh api /app/installations \
                --jq '.[0].id' | xargs -I {} \
                gh api /app/installations/{}/access_tokens \
                -X POST --jq .token)
              TOKEN_CREATED=$(date +%s)

              # Retry operation
              gh api user --jq .login
            fi

            echo "Iteration $ITERATION completed at $(date)"
            ((ITERATION++))
            sleep 60
          done

Error Handling for Expired Tokens

Detect and Recover from Expiration

name: Token Expiration Error Handling

on:
  workflow_dispatch:

jobs:
  error-recovery:
    runs-on: ubuntu-latest
    steps:
      - name: Operation with expiration handling
        env:
          APP_ID: ${{ secrets.CORE_APP_ID }}
          PRIVATE_KEY: ${{ secrets.CORE_APP_PRIVATE_KEY }}
        run: |
          # Function to generate fresh token
          generate_token() {
            gh api /app/installations \
              --jq '.[0].id' | xargs -I {} \
              gh api /app/installations/{}/access_tokens \
              -X POST --jq .token
          }

          # Function to perform API call with retry on 401
          api_call_with_retry() {
            local max_retries=3
            local retry_count=0

            while [ $retry_count -lt $max_retries ]; do
              # Attempt API call
              if gh api user --jq .login 2>/tmp/api_error; then
                return 0
              fi

              # Check error type
              if grep -q "401" /tmp/api_error || grep -q "Bad credentials" /tmp/api_error; then
                echo "::warning::Token expired (attempt $((retry_count + 1))/$max_retries)"

                # Refresh token
                export GH_TOKEN=$(generate_token)
                echo "::notice::Token refreshed"

                ((retry_count++))
                sleep 2
              else
                # Non-expiration error - fail
                cat /tmp/api_error
                return 1
              fi
            done

            echo "::error::Failed after $max_retries retries"
            return 1
          }

          # Initial token
          export GH_TOKEN=$(generate_token)

          # Perform operations
          for i in {1..100}; do
            if ! api_call_with_retry; then
              echo "::error::Operation failed at iteration $i"
              exit 1
            fi
            sleep 60
          done

Comments