返回博客

GitHub Actions 和 CI/CD 流水线中的代理:带代码示例的完整指南

我们来探讨如何将代理连接到 GitHub Actions 工作流,以便自动任务不会被阻止并能在所需区域运行。

📅2026年7月20日
```html

GitHub Actions 是一个强大的自动化工具:它运行测试、部署应用程序、收集数据并执行其他数十项任务。但是,一旦工作流开始访问外部资源——市场、广告平台、国外 API——它就会立即面临地理封锁和 IP 限制。解决方案只有一个:在管道中直接连接代理。

为什么在 GitHub Actions 中使用代理:真实场景

许多团队不仅使用 GitHub Actions 来部署代码,还用于自动化业务任务:监控竞争对手的价格、从市场收集数据、自动检查广告账户和从不同地区测试网站。所有这些任务都有一个共同的问题——GitHub Actions 的运行器具有来自 Microsoft Azure 的固定 IP,许多服务会阻止或限制它。

以下是一些具体情况,没有代理无法解决:

  • 解析 Wildberries、Ozon、Avito——这些平台早已将云服务提供商的 IP 范围列入黑名单。来自 GitHub Actions 运行器的请求将在第 2-3 次尝试时被阻止或收到验证码。
  • 地理定位测试——市场营销人员和 QA 工程师检查网站或广告在莫斯科、柏林或纽约的用户眼中的样子。没有代理,运行器总是“看到”来自一个地区的内容。
  • 与区域限制的 API 交互——某些 API(例如,特定设置的 Google Ads 区域版本、Facebook 营销 API)根据请求的地理位置返回不同的数据。
  • 竞争对手监控——自动收集价格、促销和产品组合需要定期请求,这些请求很容易通过重复的 IP 数据中心被检测到。
  • 自动化广告检查——套利者和绩效营销人员通过 CI/CD 中的脚本启动广告状态、余额和指标的自动检查。
  • 与外部服务的集成测试——某些服务出于安全原因阻止来自 Azure 范围的请求,测试会毫无解释地失败。

在所有这些情况下,代理从根本上解决了问题:工作流开始看起来像是来自所需城市的普通用户的请求,而不是来自 Microsoft 的云服务器。

GitHub Actions 如何与网络协作

在设置代理之前,重要的是要理解 GitHub Actions 中的网络架构。当您在标准的 ubuntu-latest 运行器上启动工作流时,任务在 Microsoft Azure 的基础设施中的虚拟机上执行。每台这样的机器都有来自 Azure 范围的公共 IP——外部服务看到的正是这个 IP。

GitHub Actions 网络的关键特性:

  • 每次启动时 IP 会更改——但仍然在已知的 Azure 范围内,这些范围很容易被检测到。
  • 没有内置的代理支持——GitHub 不提供原生的流量代理机制。
  • 环境变量全局有效——如果在作业级别设置 HTTP_PROXY,该作业内的所有步骤将使用代理。
  • 自托管运行器——一种替代方案,您在自己的服务器上运行运行器。在这种情况下,代理在服务器级别而不是工作流级别进行设置。

对于大多数任务,最佳方法是在工作流文件中通过环境变量设置代理(.github/workflows/your-workflow.yml)。这是一种通用方法,适用于大多数工具:curl、wget、Python requests、Node.js http、Go net/http 等等。

选择哪种类型的代理用于 CI/CD

选择代理类型取决于任务。对于 CI/CD 管道,有三种相关选项,每种都有其特定的用途:

代理类型 适用于哪些任务 速度 信任级别
住宅代理 解析受保护的网站、地理定位、监控市场 中等 高——真实家庭 IP
移动代理 测试移动版本、社交媒体工作、Facebook/TikTok API 中等 最高——运营商 IP
数据中心代理 集成测试、请求不受保护的 API、高负载 中等

实用规则:如果您的工作流解析 Wildberries、Ozon 或其他具有反机器人保护的市场——请选择住宅代理。如果您正在测试 Facebook Ads 或 TikTok Ads 的广告账户——请选择移动代理。对于简单的集成测试和对开放 API 的请求,数据中心代理就足够了:它们更快且更便宜。

💡 关于协议的重要信息

对于 GitHub Actions,优先使用 HTTP/HTTPS 代理——它们被大多数工具支持,无需额外设置。SOCKS5 也可以工作,但需要在每个工具中明确指定。如果您的提供商支持这两种协议——请从 HTTP 开始。

通过环境变量设置代理

在 GitHub Actions 中连接代理的最通用方法是设置标准环境变量 HTTP_PROXYHTTPS_PROXYNO_PROXY。大多数命令行工具和编程语言会自动识别它们。

使用代理的工作流的基本结构如下:

name: Workflow with Proxy

on:
  schedule:
    - cron: '0 9 * * *'
  workflow_dispatch:

jobs:
  scrape-data:
    runs-on: ubuntu-latest

    env:
      HTTP_PROXY: http://${{ secrets.PROXY_USER }}:${{ secrets.PROXY_PASS }}@${{ secrets.PROXY_HOST }}:${{ secrets.PROXY_PORT }}
      HTTPS_PROXY: http://${{ secrets.PROXY_USER }}:${{ secrets.PROXY_PASS }}@${{ secrets.PROXY_HOST }}:${{ secrets.PROXY_PORT }}
      NO_PROXY: localhost,127.0.0.1,github.com

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Check current IP (用于检查)
        run: curl -s https://api.ipify.org

      - name: Run main script
        run: python scripts/scraper.py

请注意 NO_PROXY 块——需要在其中添加不需要代理流量的地址。至少包括 localhost127.0.0.1。还建议添加 github.com,以便与存储库的操作(checkout、push)直接进行。

如果代理没有身份验证(只有 IP 和端口),格式会简化:

env:
  HTTP_PROXY: http://203.0.113.10:8080
  HTTPS_PROXY: http://203.0.113.10:8080
  NO_PROXY: localhost,127.0.0.1

对于 SOCKS5 代理,只需更改 URL 中的方案:

env:
  HTTP_PROXY: socks5://user:password@proxy-host:1080
  HTTPS_PROXY: socks5://user:password@proxy-host:1080

curl、wget 和 shell 中的 HTTP 请求的代理

如果环境变量在作业级别设置(如上所示),curlwget 会自动识别它们。但有时需要明确传递代理——例如,对于特定步骤或调试时。

在 curl 中明确指定代理:

- name: Fetch data with proxy
  run: |
    curl -x http://${{ secrets.PROXY_USER }}:${{ secrets.PROXY_PASS }}@${{ secrets.PROXY_HOST }}:${{ secrets.PROXY_PORT }} \
      -s \
      -o output.json \
      https://api.example.com/data

    # 通过代理检查
    curl -x http://${{ secrets.PROXY_USER }}:${{ secrets.PROXY_PASS }}@${{ secrets.PROXY_HOST }}:${{ secrets.PROXY_PORT }} \
      -s https://api.ipify.org?format=json

对于 wget:

- name: Download with wget via proxy
  run: |
    wget -e "https_proxy=http://${{ secrets.PROXY_USER }}:${{ secrets.PROXY_PASS }}@${{ secrets.PROXY_HOST }}:${{ secrets.PROXY_PORT }}" \
      -q \
      -O data.html \
      https://target-site.com/page

一个有用的调试步骤是在工作流开始时检查 IP 地址。如果代理正常工作,您将看到代理服务器的 IP,而不是 Azure 的 IP:

- name: Verify proxy is active
  run: |
    echo "=== 无代理的 IP ==="
    curl -s --noproxy '*' https://api.ipify.org || echo "直接请求失败"
    echo ""
    echo "=== 通过代理的 IP ==="
    curl -s https://api.ipify.org

在工作流中使用 Python 脚本的代理

Python 是 CI/CD 中最流行的脚本语言之一。requests 库会自动读取环境变量 HTTP_PROXYHTTPS_PROXY,如果它们被设置。但为了更灵活的控制,最好明确传递代理。

通过环境变量明确传递代理的 Python 脚本示例:

import os
import requests

# 从环境变量读取代理数据
proxy_host = os.environ.get('PROXY_HOST')
proxy_port = os.environ.get('PROXY_PORT')
proxy_user = os.environ.get('PROXY_USER')
proxy_pass = os.environ.get('PROXY_PASS')

proxies = {
    'http': f'http://{proxy_user}:{proxy_pass}@{proxy_host}:{proxy_port}',
    'https': f'http://{proxy_user}:{proxy_pass}@{proxy_host}:{proxy_port}',
}

# 在请求中使用代理
response = requests.get(
    'https://www.wildberries.ru/catalog/123456/detail.aspx',
    proxies=proxies,
    timeout=30,
    headers={
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
    }
)

print(f"状态: {response.status_code}")
print(f"内容长度: {len(response.content)}")

在工作流文件中,您需要将变量作为单独的秘密传递(而不是作为完整的 URL),以便脚本能够收集它们:

- name: Run Python scraper
  env:
    PROXY_HOST: ${{ secrets.PROXY_HOST }}
    PROXY_PORT: ${{ secrets.PROXY_PORT }}
    PROXY_USER: ${{ secrets.PROXY_USER }}
    PROXY_PASS: ${{ secrets.PROXY_PASS }}
  run: python scripts/scraper.py

对于使用 Playwright 或 Selenium 的 Python,代理配置略有不同:

# Playwright
from playwright.sync_api import sync_playwright
import os

proxy_url = f"http://{os.environ['PROXY_USER']}:{os.environ['PROXY_PASS']}@{os.environ['PROXY_HOST']}:{os.environ['PROXY_PORT']}"

with sync_playwright() as p:
    browser = p.chromium.launch(
        proxy={
            "server": proxy_url
        }
    )
    page = browser.new_page()
    page.goto("https://target-site.com")
    # ... 后续逻辑
    browser.close()

Node.js 和 npm 任务中的代理

Node.js 不会自动读取系统变量 HTTP_PROXY——需要使用特殊库或明确设置代理。最方便的选项是 https-proxy-agentaxios,并配置代理。

// 使用 axios
const axios = require('axios');

const proxyConfig = {
  host: process.env.PROXY_HOST,
  port: parseInt(process.env.PROXY_PORT),
  auth: {
    username: process.env.PROXY_USER,
    password: process.env.PROXY_PASS
  }
};

async function fetchData(url) {
  try {
    const response = await axios.get(url, {
      proxy: proxyConfig,
      timeout: 30000,
      headers: {
        'User-Agent': 'Mozilla/5.0 (compatible; MyBot/1.0)'
      }
    });
    return response.data;
  } catch (error) {
    console.error(`请求失败: ${error.message}`);
    throw error;
  }
}

fetchData('https://api.example.com/prices')
  .then(data => console.log(JSON.stringify(data, null, 2)))
  .catch(() => process.exit(1));

对于 npm 命令(例如,如果 npm 尝试通过企业代理下载包),配置更简单:

- name: Configure npm proxy
  run: |
    npm config set proxy http://${{ secrets.PROXY_USER }}:${{ secrets.PROXY_PASS }}@${{ secrets.PROXY_HOST }}:${{ secrets.PROXY_PORT }}
    npm config set https-proxy http://${{ secrets.PROXY_USER }}:${{ secrets.PROXY_PASS }}@${{ secrets.PROXY_HOST }}:${{ secrets.PROXY_PORT }}

- name: Install dependencies
  run: npm install

- name: Reset npm proxy (使用后清除)
  run: |
    npm config delete proxy
    npm config delete https-proxy

在 GitHub Secrets 中安全存储代理数据

永远不要将代理数据(主机、端口、用户名、密码)直接存储在工作流文件中。这是一个严重的安全错误:工作流文件存储在代码库中,可能对项目的所有参与者可见,甚至是公开的。

正确的方法是使用 GitHub Secrets。 这是逐步指南:

  1. 在 GitHub 上打开代码库
  2. 转到 Settings → Secrets and variables → Actions
  3. 点击 New repository secret
  4. 创建四个秘密:PROXY_HOSTPROXY_PORTPROXY_USERPROXY_PASS
  5. 在工作流中通过语法 ${{ secrets.PROXY_HOST }} 引用它们

🔒 额外的安全措施

  • 如果不同环境(staging/production)使用不同的代理,请使用 Environment secrets 而不是 Repository secrets
  • 通过 Environment protection rules 限制对秘密的访问——要求在生产环境中手动确认
  • 定期轮换代理凭据——每 30-90 天更改密码
  • 不要通过 echo 将秘密值输出到日志——GitHub 会自动屏蔽它们,但最好不要冒险

如果您使用轮换代理(每次请求时 IP 更改或按计划更改),通常只需存储一个端点——代理提供商自己管理 IP 池。在这种情况下,秘密中只会有一个轮换网关的主机和端口。

代理轮换和错误处理

即使是高质量的代理有时也会出现故障:IP 可能会被暂时禁止,会话可能会中断,服务器可能没有响应。对于自动运行的 CI/CD 管道,重要的是考虑到这些情况的处理。

策略 1:使用相同的代理重试

import requests
import time
import os

def fetch_with_retry(url, max_retries=3, delay=5):
    proxies = {
        'http': f"http://{os.environ['PROXY_USER']}:{os.environ['PROXY_PASS']}@{os.environ['PROXY_HOST']}:{os.environ['PROXY_PORT']}",
        'https': f"http://{os.environ['PROXY_USER']}:{os.environ['PROXY_PASS']}@{os.environ['PROXY_HOST']}:{os.environ['PROXY_PORT']}",
    }

    for attempt in range(max_retries):
        try:
            response = requests.get(url, proxies=proxies, timeout=30)
            response.raise_for_status()
            return response
        except requests.exceptions.RequestException as e:
            print(f"尝试 {attempt + 1} 失败: {e}")
            if attempt < max_retries - 1:
                print(f"{delay} 秒后重试...")
                time.sleep(delay)
                delay *= 2  # 指数延迟
    raise Exception(f"所有 {max_retries} 次尝试均失败,网址:{url}")

策略 2:代理列表切换

如果您有多个代理服务器,可以将它们的列表存储在一个秘密中(用逗号分隔),并在出错时切换:

import os
import requests
import random

# 秘密 PROXY_LIST 包含: "host1:port1:user1:pass1,host2:port2:user2:pass2"
proxy_list_raw = os.environ.get('PROXY_LIST', '').split(',')

def parse_proxy(proxy_str):
    parts = proxy_str.strip().split(':')
    if len(parts) == 4:
        host, port, user, password = parts
        return {
            'http': f'http://{user}:{password}@{host}:{port}',
            'https': f'http://{user}:{password}@{host}:{port}',
        }
    return None

proxies = [p for p in [parse_proxy(raw) for raw in proxy_list_raw] if p]

def fetch_with_proxy_rotation(url):
    random.shuffle(proxies)  # 随机顺序
    for proxy in proxies:
        try:
            response = requests.get(url, proxies=proxy, timeout=20)
            if response.status_code == 200:
                return response
        except Exception as e:
            print(f"代理失败: {e}, 尝试下一个...")
    raise Exception("所有代理均已耗尽")

策略 3:使用轮换端点

最简单的选项是使用具有单一轮换网关的代理提供商。在这种情况下,您连接到一个地址,而提供商会自动从池中提供不同的 IP。代码中不需要任何轮换逻辑——只需一行连接即可。

真实场景:解析、测试、监控价格

让我们看看三个具体场景,这些场景在使用 GitHub Actions 和代理的团队中最常见。

场景 1:每日监控 Wildberries 的价格

市场卖家经常设置自动收集竞争对手价格的工作流。工作流按计划启动(例如,每天早上 7:00),收集数据并将其保存到 Google Sheets 或发送到 Telegram。

name: Daily Price Monitor

on:
  schedule:
    - cron: '0 4 * * *'  # 07:00 MSK (UTC+3)

jobs:
  monitor-prices:
    runs-on: ubuntu-latest

    env:
      HTTP_PROXY: http://${{ secrets.PROXY_USER }}:${{ secrets.PROXY_PASS }}@${{ secrets.PROXY_HOST }}:${{ secrets.PROXY_PORT }}
      HTTPS_PROXY: http://${{ secrets.PROXY_USER }}:${{ secrets.PROXY_PASS }}@${{ secrets.PROXY_HOST }}:${{ secrets.PROXY_PORT }}
      NO_PROXY: github.com,api.github.com

    steps:
      - uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: pip install requests beautifulsoup4 gspread

      - name: Run price scraper
        env:
          GOOGLE_SHEETS_KEY: ${{ secrets.GOOGLE_SHEETS_KEY }}
          TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
          TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
        run: python scripts/price_monitor.py

      - name: Upload results artifact
        uses: actions/upload-artifact@v4
        with:
          name: price-data-${{ github.run_id }}
          path: output/prices.json

场景 2:地理定位网站测试

市场营销人员和 QA 团队使用代理检查网站或广告在不同城市的用户眼中的样子。特别适合检查区域价格、内容和重定向。

name: Geo-targeted Site Tests

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test-moscow:
    runs-on: ubuntu-latest
    name: Test from Moscow
    steps:
      - uses: actions/checkout@v4
      - name: Run geo tests (RU/Moscow proxy)
        env:
          HTTP_PROXY: http://${{ secrets.PROXY_RU_USER }}:${{ secrets.PROXY_RU_PASS }}@${{ secrets.PROXY_RU_HOST }}:${{ secrets.PROXY_RU_PORT }}
          HTTPS_PROXY: http://${{ secrets.PROXY_RU_USER }}:${{ secrets.PROXY_RU_PASS }}@${{ secrets.PROXY_RU_HOST }}:${{ secrets.PROXY_RU_PORT }}
        run: |
          python tests/geo_test.py --region=RU --city=Moscow

  test-germany:
    runs-on: ubuntu-latest
    name: Test from Germany
    steps:
      - uses: actions/checkout@v4
      - name: Run geo tests (DE proxy)
        env:
          HTTP_PROXY: http://${{ secrets.PROXY_DE_USER }}:${{ secrets.PROXY_DE_PASS }}@${{ secrets.PROXY_DE_HOST }}:${{ secrets.PROXY_DE_PORT }}
          HTTPS_PROXY: http://${{ secrets.PROXY_DE_USER }}:${{ secrets.PROXY_DE_PASS }}@${{ secrets.PROXY_DE_HOST }}:${{ secrets.PROXY_DE_PORT }}
        run: |
          python tests/geo_test.py --region=DE

场景 3:自动检查广告账户

套利者和绩效营销人员经常使用 GitHub Actions 自动检查 Facebook Ads 广告账户的状态、余额和指标。来自 Azure 范围的请求可能会引发额外的安全检查——代理有助于绕过这一点。

name: Ad Account Health Check

on:
  schedule:
    - cron: '*/30 6-22 * * *'  # 每 30 分钟从 6 到 22 MSK

jobs:
  check-accounts:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: pip install requests

      - name: Check Facebook Ads accounts
        env:
          HTTP_PROXY: http://${{ secrets.PROXY_USER }}:${{ secrets.PROXY_PASS }}@${{ secrets.PROXY_HOST }}:${{ secrets.PROXY_PORT }}
          HTTPS_PROXY: http://${{ secrets.PROXY_USER }}:${{ secrets.PROXY_PASS }}@${{ secrets.PROXY_HOST }}:${{ secrets.PROXY_PORT }}
          FB_ACCESS_TOKEN: ${{ secrets.FB_ACCESS_TOKEN }}
          ACCOUNT_IDS: ${{ secrets.FB_ACCOUNT_IDS }}
          TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
          TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
        run: python scripts/check_fb_accounts.py

📋 启动代理工作流前的检查清单

  • ✅ 代理数据已添加到 GitHub Secrets(而不是工作流文件中)
  • ✅ 变量 NO_PROXY 包括 github.com
  • ✅ 添加了 IP 检查步骤以进行调试
  • ✅ 实现了错误处理和重试逻辑
  • ✅ 代理类型与任务相符(受保护网站使用住宅代理)
  • ✅ 设置了错误通知(Telegram、Slack 或电子邮件)
  • ✅ 在添加调度之前,通过 workflow_dispatch 手动测试了工作流

结论

在 GitHub Actions 中设置代理并不是一项复杂的任务,只要知道正确的方法。本指南的关键要点:

  • 环境变量 HTTP_PROXY / HTTPS_PROXY——一种通用方法,适用于大多数工具,无需更改代码。
  • GitHub Secrets——存储代理凭据的唯一正确位置。
  • 代理类型很重要:对于解析受保护的市场,需要住宅 IP,对于广告平台——需要移动代理,对于简单的 API 请求,数据中心代理就足够了。
  • 重试逻辑是必需的,适用于按计划无监督运行的管道。
  • 工作流开头的 IP 检查步骤将节省调试时间。

如果您的 GitHub Actions 工作流与市场、广告平台或任何具有反机器人保护的服务交互,建议使用 住宅代理——它们具有真实的家庭用户 IP,并且比 GitHub 服务器的云地址更少引发封锁。对于与 Facebook Ads、TikTok 或其他社交平台相关的任务,最佳选择是 移动代理,它们具有运营商 IP——为平台提供了最大的信任级别。

```