昨天逛博客看到了使用 GitHub Actions 定时调用 Microsoft 365 E5 API 以帮助续订的文章,我转念一想这是不是也可以用来跑定时任务打卡呢?说干就干我开了一个小的坑:用 GitHub Actions 跑之前写的 Python 打卡脚本。

照葫芦环节

参考项目是 wangziyingwen/AutoApiSecret,他的工作流 文件

  1. 事先将一些重要信息写在仓库 Secrets 中,以 name=value 的形式定义
  2. 在 workflow 中将 Secrets 内容 echo 写入临时文本,并将脚本复制一份
  3. 在 workflow 中使用 sed -i '10 r tmp.txt' tmp.py
    没怎么了解过 sed 这个命令,运行后发现它将 tmp.txt 的内容写入了 tmp.py 的指定行 11 。哦,在执行工作流的过程中将私密信息赋值写入代码前面,避免将私密信息直接写在公开仓库中
  4. 在 workflow 中执行 python tmp.py 运行填入了 Serects 的临时文件
  5. 在 workflow 中删除所有临时文件并 commit 历史记录到仓库
  6. 以上是一次工作流,定时任务靠 on.schedule.cron 实现

让我自己写出这么一个流程我肯定是不会的:不了解 GitHub Actions 怎么用 Secrets 和 on.schedule ,不熟悉 Linux sed 命令,也没有在工作流中新建临时文件这样的想法…… 我就这么被拒绝白嫖了。

画瓢环节

此刻,白嫖是第一生产力。了解了这样的流程部署自己的自动打卡(让 GitHub 定时执行 python checkin.py)就不是什么难事了。照葫芦画瓢,我的瓢诞生了:

name: Auto Checkin

on: 
  release:
    types: [published]
  push:
    tags:
    - 'v*'
  # Coordinated Universal Time (UTC)
  schedule:
    - cron: '0 0 * * *'           # 定时任务实现方式
  watch:
    types: [started]
   
jobs:
  build:
    runs-on: ubuntu-latest
    if: github.event.repository.owner.id == github.event.sender.id  # 自己点的 star
    steps:
      - name: Checkout
        uses: actions/[email protected]
      - name: Python Setup
        uses: actions/[email protected]
        with:
          python-version: 3.8
      - name: Pip Cache             # 按照官方仓库 actions/cache 添加
        uses: actions/[email protected]
        with:
          path: ~/.cache/pip        # Ubuntu 的缓存位置,不同系统不同位置需要修改
          key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
          restore-keys: ${{ runner.os }}-pip-
      - name: Addons Install        # 安装脚本必须组件 lxml requests
        run: pip install lxml requests
      - name: Secrets Get           # 获取 Secrets
        env: 
          SECRET_ID: ${{ secrets.SECRET_ID }}
          SECRET_PASS: ${{ secrets.SECRET_PASS }}
          SECRET_BOUND: ${{ secrets.SECRET_BOUND }}
          SECRET_DATA: ${{ secrets.SECRET_DATA }}
        # 先复制一个临时文件,然后写入 Secrets 到文本,再将其写入临时脚本文件指定行
        run: | 
          cp checkin.py action.py
          echo $SECRET_ID > action-id.txt
          echo $SECRET_PASS > action-pass.txt
          echo $SECRET_BOUND > action-bound.txt
          echo $SECRET_DATA > action-data.txt
          sed -i '19 r action-id.txt' action.py
          sed -i '20 r action-pass.txt' action.py
          sed -i '21 r action-bound.txt' action.py
          sed -i '22 r action-data.txt' action.py
      - name: Checkin Action
        env:
          TZ: Asia/Shanghai         # 设定时区为北京时间
        # 工作流过程中新建 log 文件夹存放待会发布到另外一个分支的内容
        run: | 
          mkdir log
          echo `date +"%Y-%m-%d %H:%M:%S %A"` >> log/time.log
          python action.py >> log/time.log
      - name: Secrets Delete        # 删除临时文件
        run: rm -f action*
      - name: Deploy Log            # 发布 log 文件夹下的记录文件到 log 分支
        uses: docker://peaceiris/gh-pages:v2
        env:
          TZ: Asia/Shanghai
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PUBLISH_BRANCH: log
          PUBLISH_DIR: ./log
        with:
          emptyCommits: false

第一版的工作流程看起来有点臃肿,不过管他呢,确实能用。

我原本以为在 GitHub Actions 中实现定时任务要很复杂的配置,毕竟每次工作流都是相当于在一个全新的服务器上执行吧。现在发现原来定时任务只需要在工作流的触发事件中写入 schedule 即可,跟上面的 L10-11 似的。在查找文档时我发现这点在官方文档中有详细说明,害,都是不会看文档惹的祸。

润色 Round 1

既然实现定时任务这样简单那有什么值得记录呢?于是我顺便给这个工作流引入了 pip 模块缓存、发布 log 记录文件到分支这两个小功能,也算是补上了之前折腾工作流学到的。

  • pip 模块缓存

这是从苏卡大大向 Cloudflare Workers Sites 部署 Hexo 博客的 文章 中学到的。Node.js 项目构建时需要的依赖挺多,没有缓存的话每次 GitHub Action 得跑很长分钟,于是苏卡大大给出了在 GitHub Actions 中缓存 node_modules 的办法: uses: actions/[email protected] 检查缓存特征 Key 是否存在(比如 Node.js 就检测 package-lock.json 文件)。

GitHub Actions 使用这一特性其实很简单,只要在 actions/cache 中找到自己需要缓存的类型确定好监测的特定路径和文件,然后添加类似上方 L26-31 的步骤在安装依赖前即可,工作流结束 GitHub 会自行监测是否需要更新缓存。我在尝试过程中将一个两分钟的用于构建并发布站点的工作流成功优化到一分多,提升还是蛮大的。

在这个项目中缓存 pip 模块需要做的就是照葫芦画瓢检查 ~/.cache/pip 目录下 requirements.txt 文件。

  • 发布 log 记录文件到分支

这个操作是从 Typecho-Theme-VOID 二次开发过程中学到的。主题自带的 workflow 流程中将构建完毕的主题文件存放在 build 文件夹下,然后发布到 nightly 分支。想上面给出的 L61-69 类似,仅需在你的 workflows 中给 uses: docker://peaceiris/gh-pages:v2 添加两个环境变量 PUBLISH_BRANCHPUBLISH_DIR 即可。

此外,借助 GitHub Actions 我还告别了在 Python 中配置 SMTP 发送打卡邮件提醒,因为 只需要开启 GitHub 工作流的运行提醒就好啦 配置起来如果像上面一样一条一条添加 Secrets 的话工作量太大。虽然 GitHub Actions 也有邮件提醒,但是它提醒的是工作流执行状况,并不能等价于打卡脚本的执行状态。这一点还有待优化。

润色 Round 2

也许看官早就想说了:为什么引用 Secrets 那么繁琐呢?又是使用环境变量的、又是将环境变量 echo.txt 文件的、又是将 .txt sed 写入 .py 的,不能简单点吗?确实,在朋友 XYenon 的指导下我得知 Python 可以通过 os.environ 直接读取环境变量,所以简单的办法来了,在 Python 脚本中将原来的赋值改写成下面的格式直接读取环境变量

import os

myid = os.environ ['SECRET_ID']

直接读入环境变量 SECRET_ID 的值并赋给 myid,在上方 workflow 中 Secrets Get、Checkin Action、Secrets Delete 三步直接合并为一步:

      # 获取机密,执行签到并将此次签到记录写入历史记录后面
      - name: Action Execute
        env:
          TZ: Asia/Shanghai
          SECRET_ID: ${{ secrets.SECRET_ID }}
          SECRET_PASS: ${{ secrets.SECRET_PASS }}
          SECRET_BOUND: ${{ secrets.SECRET_BOUND }}
          SECRET_DATA: ${{ secrets.SECRET_DATA }}
        run: python checkin.py | tee -a checkin-python.log

一下子心情舒畅了不少!

润色 Round 3

上次那篇关于 Python 打卡的 文章XYenon 给出了仅需用户名和密码的 Ruby 版本,代码在 Gist

代码仅 60 行,第一次看完我大呼“妙啊”,只要脚本代替人执行 确认信息 + 提交表单 两步就完事了。现有的 Python 打卡每次都将事先定义的表单数据提交一遍,不考虑打卡系统中表单在服务器的缓存,在 GitHub Actions 下运行起来很直接。但如果表单数据在服务器上一直都有缓存,那 Python 每次提交表单的行为看起来就有点笨了,用这个 Ruby 替换我觉得无论是在工作效率上还是 Actions 部署上应该都会更好。

与 Python 类似,Ruby 也可以在代码中使用 ENV['SECRET_ID'] 这样的语句直接获取环境变量。

实际调试的时候,我发现这看起来简单的代码部署起来也不容易…… Ruby 使用 webdrivers 库来在终端驱动一个 headless Chrome 浏览器,然后执行动作。抛开因为不熟悉 Ruby + webdrivers 这套环境使我在 GitHub Actions 工作流写法上花的时间,这个脚本实际执行的效率也比较低,Python 直白的提交表单整个工作流程需要 30 秒左右,而 Ruby 模拟 Chrome 操作花了三分钟多(不排除因为 GitHub 服务器与学校服务器连接不稳定的原因)。是为了更快的部署而选择 Python 打卡呢?还是为了更快的 workflow 选择 Ruby 打卡呢?

经过多方搜索我使用了这样的 GitHub Actions 环境跑 Ruby + Watir + webdrivers 代码,不知道有没有更好的方式:

jobs:
  build:
    runs-on: ubuntu-latest
    # 用于运行 headless chrome 的服务
    services:
      hub:
        image: selenium/hub:3.141.59-gold
        env:
          SELENIUM_HUB_HOST: localhost
      chrome:
        image: selenium/node-chrome:3.141.59-gold
        env:
          HUB_HOST: localhost
          HUB_PORT: 9515
    # 只有自己点 Star 才触发
    if: github.event.repository.owner.id == github.event.sender.id
    steps:
      - name: Checkout
        uses: actions/[email protected]
      - name: Ruby Setup
        uses: actions/[email protected]
        with:
          ruby-version: 2.5.x
      # 安装 gem 模块
      - name: Addons Install
        run: gem install watir webdrivers
      # 获取机密,执行签到并将此次签到记录写入历史记录后面
      - name: Action Execute
        env:
          TZ: Asia/Shanghai
          LANG: zh_CN.UTF-8         # 应对校园网页 I10n
          SECRET_ID: ${{ secrets.SECRET_ID }}
          SECRET_PASS: ${{ secrets.SECRET_PASS }}
        run: ruby checkin.rb | tee -a checkin-ruby.log

也许 Ruby 版本的打卡程序更适合写成 JavaScript 用户脚本交给浏览器插件执行。

结语

以上仅是 GitHub Actions 妙用的冰山一角,之前关注的一个博客 P3TERX ZONE 中写了挺多关于 GitHub Actions 的文章,有时间的话我也要去学习学习!让我想想我还有什么定时任务之类的东西能放到 GitHub 上白嫖……

哦,忘记放 GitHub 仓库链接了,monsterxcn/HEU-Checkin-COVID-19,有一说一我觉得我的 Readme 写的真好 ,智力健全的人应该都能自行部署