From 964781ace6524368f16430735a7842b6b4b96598 Mon Sep 17 00:00:00 2001 From: Kevin Koech Date: Thu, 24 Sep 2026 12:24:45 +0300 Subject: [PATCH 01/10] ci(pesacheck_meedan_bridge): Add continuous deployment workflow Mirror the TwoopsTracker CD workflow for the bridge: on a push to main that changes pesacheck_meedan_bridge/py/VERSION, lint, build the Docker image with Pants, push it to DockerHub and deploy to Dokku. --- .../workflows/pesacheck-meedan-bridge-cd.yaml | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 .github/workflows/pesacheck-meedan-bridge-cd.yaml diff --git a/.github/workflows/pesacheck-meedan-bridge-cd.yaml b/.github/workflows/pesacheck-meedan-bridge-cd.yaml new file mode 100644 index 00000000..81a64035 --- /dev/null +++ b/.github/workflows/pesacheck-meedan-bridge-cd.yaml @@ -0,0 +1,79 @@ +# yamllint disable rule:line-length +name: PesaCheck Meedan Bridge | Continuous Deployment +"on": + push: + branches: + - main + paths: + - "pesacheck_meedan_bridge/py/VERSION" +env: + DOKKU_REMOTE_BRANCH: "master" + DOKKU_REMOTE_URL: "ssh://dokku@dokku-1.dev.codeforafrica.org/pesacheck-meedan-bridge" + GIT_PUSH_FLAGS: "--force" + IMAGE_NAME: "codeforafrica/pesacheck_meedan_bridge" + +# This allows a subsequently queued workflow run to interrupt previous runs +concurrency: + group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" + cancel-in-progress: true + +jobs: + build: + name: Build and Deploy + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11"] + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Set env + run: echo "VERSION=$(cat pesacheck_meedan_bridge/py/VERSION)" >> $GITHUB_ENV + + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + + - name: Initialize Pants + uses: pantsbuild/actions/init-pants@main + with: + # cache0 makes it easy to bust the cache if needed + gha-cache-key: cache0-py${{ matrix.python_version }} + named-caches-hash: ${{ hashFiles('lockfiles/*.json', '**/something-else.lock') }} + + - name: Bootstrap Pants + run: | + pants --version + + - name: Check BUILD files + run: ./pants tailor --check update-build-files --check + + - name: Lint + run: | + pants lint --lint-skip-formatters pesacheck_meedan_bridge/:: + + - name: Build Docker image + run: | + VERSION=${{ env.VERSION }} ./pants package pesacheck_meedan_bridge/docker/:: + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Push to DockerHub + run: | + docker push ${{ env.IMAGE_NAME }}:${{ env.VERSION }} + + - name: Push to Dokku + uses: dokku/github-action@v1.9.0 + with: + branch: ${{ env.DOKKU_REMOTE_BRANCH }} + git_push_flags: ${{ env.GIT_PUSH_FLAGS }} + git_remote_url: ${{ env.DOKKU_REMOTE_URL }} + ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }} From 8d2db59fc7bba84e769a0b7aa6066da63087aa2a Mon Sep 17 00:00:00 2001 From: Kevin Koech Date: Thu, 24 Sep 2026 12:36:45 +0300 Subject: [PATCH 02/10] ci(pesacheck_meedan_bridge): Test CD workflow from this branch Temporarily runs the workflow on pushes to chore/pesacheck-bridge-cd and bumps VERSION to 0.1.19 so the paths filter matches. The branch entry is removed before merging. --- .github/workflows/pesacheck-meedan-bridge-cd.yaml | 2 ++ pesacheck_meedan_bridge/py/VERSION | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pesacheck-meedan-bridge-cd.yaml b/.github/workflows/pesacheck-meedan-bridge-cd.yaml index 81a64035..bf689789 100644 --- a/.github/workflows/pesacheck-meedan-bridge-cd.yaml +++ b/.github/workflows/pesacheck-meedan-bridge-cd.yaml @@ -4,6 +4,8 @@ name: PesaCheck Meedan Bridge | Continuous Deployment push: branches: - main + # TODO: remove before merging; here only to test the workflow itself. + - chore/pesacheck-bridge-cd paths: - "pesacheck_meedan_bridge/py/VERSION" env: diff --git a/pesacheck_meedan_bridge/py/VERSION b/pesacheck_meedan_bridge/py/VERSION index f8bc4c62..d8a023ec 100644 --- a/pesacheck_meedan_bridge/py/VERSION +++ b/pesacheck_meedan_bridge/py/VERSION @@ -1 +1 @@ -0.1.18 +0.1.19 From 1e14f66636078222099fda31efecad69fb4ea6bf Mon Sep 17 00:00:00 2001 From: Kevin Koech Date: Thu, 24 Sep 2026 12:38:28 +0300 Subject: [PATCH 03/10] ci(pesacheck_meedan_bridge): Call pants from PATH, not ./pants The repo has no ./pants launcher script, so the tailor and package steps failed with exit 127. twoops-tracker-cd.yaml has the same two calls and would fail the same way. Also temporarily triggers on changes to the workflow file so it can be tested without bumping VERSION each time. --- .github/workflows/pesacheck-meedan-bridge-cd.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pesacheck-meedan-bridge-cd.yaml b/.github/workflows/pesacheck-meedan-bridge-cd.yaml index bf689789..3b3812d1 100644 --- a/.github/workflows/pesacheck-meedan-bridge-cd.yaml +++ b/.github/workflows/pesacheck-meedan-bridge-cd.yaml @@ -8,6 +8,8 @@ name: PesaCheck Meedan Bridge | Continuous Deployment - chore/pesacheck-bridge-cd paths: - "pesacheck_meedan_bridge/py/VERSION" + # TODO: remove before merging; here only to test the workflow itself. + - ".github/workflows/pesacheck-meedan-bridge-cd.yaml" env: DOKKU_REMOTE_BRANCH: "master" DOKKU_REMOTE_URL: "ssh://dokku@dokku-1.dev.codeforafrica.org/pesacheck-meedan-bridge" @@ -52,7 +54,7 @@ jobs: pants --version - name: Check BUILD files - run: ./pants tailor --check update-build-files --check + run: pants tailor --check update-build-files --check - name: Lint run: | @@ -60,7 +62,7 @@ jobs: - name: Build Docker image run: | - VERSION=${{ env.VERSION }} ./pants package pesacheck_meedan_bridge/docker/:: + VERSION=${{ env.VERSION }} pants package pesacheck_meedan_bridge/docker/:: - name: Login to DockerHub uses: docker/login-action@v3 From b481a55c819dc601da3b7a48c0b24476fab51bf6 Mon Sep 17 00:00:00 2001 From: Kevin Koech Date: Thu, 24 Sep 2026 12:43:01 +0300 Subject: [PATCH 04/10] ci(pesacheck_meedan_bridge): Deploy the built image via git:from-image Without deploy_docker_image, Dokku tries to build the pushed monorepo with the Heroku Python buildpack and fails: there is no top-level requirements.txt. Deploy the image pushed to DockerHub instead. --- .github/workflows/pesacheck-meedan-bridge-cd.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/pesacheck-meedan-bridge-cd.yaml b/.github/workflows/pesacheck-meedan-bridge-cd.yaml index 3b3812d1..8ccdb4fc 100644 --- a/.github/workflows/pesacheck-meedan-bridge-cd.yaml +++ b/.github/workflows/pesacheck-meedan-bridge-cd.yaml @@ -78,6 +78,9 @@ jobs: uses: dokku/github-action@v1.9.0 with: branch: ${{ env.DOKKU_REMOTE_BRANCH }} + # Deploy the image we just pushed (git:from-image) instead of having + # Dokku build this monorepo with a buildpack. + deploy_docker_image: ${{ env.IMAGE_NAME }}:${{ env.VERSION }} git_push_flags: ${{ env.GIT_PUSH_FLAGS }} git_remote_url: ${{ env.DOKKU_REMOTE_URL }} ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }} From b31ca68d4e0a74f63b9921b7925ee4c65c06cead Mon Sep 17 00:00:00 2001 From: Kevin Koech Date: Thu, 24 Sep 2026 13:24:28 +0300 Subject: [PATCH 05/10] fix(pesacheck_meedan_bridge): Send an identifying User-Agent to Ghost Requests went out as python-requests/x.y.z, which Cloudflare challenges in front of pesacheck.org: the daily run got the "Just a moment..." page instead of the Content API. Name the bridge instead. A custom User-Agent may not be enough on its own where the challenge is driven by IP reputation; the Content API path still wants a Cloudflare allowlist for the Dokku host. --- pesacheck_meedan_bridge/py/main.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pesacheck_meedan_bridge/py/main.py b/pesacheck_meedan_bridge/py/main.py index 19d7f3ec..fb2b57ad 100755 --- a/pesacheck_meedan_bridge/py/main.py +++ b/pesacheck_meedan_bridge/py/main.py @@ -29,6 +29,10 @@ def extract_summary(feed): return summary_text.strip() if summary_text else None +# Identify the bridge instead of defaulting to "python-requests/x.y.z", which +# Cloudflare challenges in front of pesacheck.org. +USER_AGENT = "pesacheck-meedan-bridge (+https://github.com/CodeForAfrica/api)" + language_codes = { "english": "en", "french": "fr", @@ -91,7 +95,7 @@ def fetch_from_pesacheck(since=None): if since: since_utc = since.astimezone(UTC).strftime("%Y-%m-%d %H:%M:%S") params["filter"] = f"published_at:>='{since_utc}'" - headers = {"Accept-Version": "v5.0"} + headers = {"Accept-Version": "v5.0", "User-Agent": USER_AGENT} posts = [] page = 1 while page: From 481d5dd0d942d919f21df6726d3cc8ce2817e2d2 Mon Sep 17 00:00:00 2001 From: Kevin Koech Date: Thu, 24 Sep 2026 13:26:14 +0300 Subject: [PATCH 06/10] chore(pesacheck_meedan_bridge): Bump VERSION to 0.1.20 Ships the identifying User-Agent for the Ghost Content API. --- pesacheck_meedan_bridge/py/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pesacheck_meedan_bridge/py/VERSION b/pesacheck_meedan_bridge/py/VERSION index d8a023ec..baa98378 100644 --- a/pesacheck_meedan_bridge/py/VERSION +++ b/pesacheck_meedan_bridge/py/VERSION @@ -1 +1 @@ -0.1.19 +0.1.20 From 727f6b15fffc052e86533059267c98ae410c840d Mon Sep 17 00:00:00 2001 From: Kevin Koech Date: Thu, 24 Sep 2026 14:07:03 +0300 Subject: [PATCH 07/10] ci(pesacheck_meedan_bridge): Drop git-push inputs from the Dokku deploy With deploy_docker_image set, dokku/github-action runs `git:from-image` and never pushes a branch, so branch/git_push_flags were dead inputs that made the step look like a git-push deploy. Name the deploy commit author instead, and rename the step to match what it does. --- .github/workflows/pesacheck-meedan-bridge-cd.yaml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pesacheck-meedan-bridge-cd.yaml b/.github/workflows/pesacheck-meedan-bridge-cd.yaml index 8ccdb4fc..9bc7dce1 100644 --- a/.github/workflows/pesacheck-meedan-bridge-cd.yaml +++ b/.github/workflows/pesacheck-meedan-bridge-cd.yaml @@ -11,9 +11,7 @@ name: PesaCheck Meedan Bridge | Continuous Deployment # TODO: remove before merging; here only to test the workflow itself. - ".github/workflows/pesacheck-meedan-bridge-cd.yaml" env: - DOKKU_REMOTE_BRANCH: "master" DOKKU_REMOTE_URL: "ssh://dokku@dokku-1.dev.codeforafrica.org/pesacheck-meedan-bridge" - GIT_PUSH_FLAGS: "--force" IMAGE_NAME: "codeforafrica/pesacheck_meedan_bridge" # This allows a subsequently queued workflow run to interrupt previous runs @@ -74,13 +72,13 @@ jobs: run: | docker push ${{ env.IMAGE_NAME }}:${{ env.VERSION }} - - name: Push to Dokku + - name: Deploy to Dokku uses: dokku/github-action@v1.9.0 with: - branch: ${{ env.DOKKU_REMOTE_BRANCH }} - # Deploy the image we just pushed (git:from-image) instead of having - # Dokku build this monorepo with a buildpack. + # With deploy_docker_image the action runs `dokku git:from-image` and + # never pushes a branch, so `branch`/`git_push_flags` don't apply. deploy_docker_image: ${{ env.IMAGE_NAME }}:${{ env.VERSION }} - git_push_flags: ${{ env.GIT_PUSH_FLAGS }} + deploy_user_name: "CodeForAfrica Bot" + deploy_user_email: "support@codeforafrica.org" git_remote_url: ${{ env.DOKKU_REMOTE_URL }} ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }} From 093d83414278c4570b10d50a091d469ec901c9bc Mon Sep 17 00:00:00 2001 From: Kevin Koech Date: Thu, 24 Sep 2026 14:31:12 +0300 Subject: [PATCH 08/10] ci(pesacheck_meedan_bridge): Drop branch-test triggers, bump to 0.1.21 The workflow now only runs on main, on a VERSION change. 0.1.20 is already built and deployed, so bump so the merge deploys cleanly instead of failing on git:from-image's "No changes detected". --- .github/workflows/pesacheck-meedan-bridge-cd.yaml | 4 ---- pesacheck_meedan_bridge/py/VERSION | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/pesacheck-meedan-bridge-cd.yaml b/.github/workflows/pesacheck-meedan-bridge-cd.yaml index 9bc7dce1..ccd554f9 100644 --- a/.github/workflows/pesacheck-meedan-bridge-cd.yaml +++ b/.github/workflows/pesacheck-meedan-bridge-cd.yaml @@ -4,12 +4,8 @@ name: PesaCheck Meedan Bridge | Continuous Deployment push: branches: - main - # TODO: remove before merging; here only to test the workflow itself. - - chore/pesacheck-bridge-cd paths: - "pesacheck_meedan_bridge/py/VERSION" - # TODO: remove before merging; here only to test the workflow itself. - - ".github/workflows/pesacheck-meedan-bridge-cd.yaml" env: DOKKU_REMOTE_URL: "ssh://dokku@dokku-1.dev.codeforafrica.org/pesacheck-meedan-bridge" IMAGE_NAME: "codeforafrica/pesacheck_meedan_bridge" diff --git a/pesacheck_meedan_bridge/py/VERSION b/pesacheck_meedan_bridge/py/VERSION index baa98378..79062996 100644 --- a/pesacheck_meedan_bridge/py/VERSION +++ b/pesacheck_meedan_bridge/py/VERSION @@ -1 +1 @@ -0.1.20 +0.1.21 From 3259989afa012202b3e71ce0627d12498e3ba3ac Mon Sep 17 00:00:00 2001 From: Kevin Koech Date: Thu, 24 Sep 2026 15:39:17 +0300 Subject: [PATCH 09/10] ci(pesacheck_meedan_bridge): Limit GITHUB_TOKEN to contents: read Flagged by CodeQL on #1209: the workflow didn't restrict the token. The job only needs to check out the repo. --- .github/workflows/pesacheck-meedan-bridge-cd.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/pesacheck-meedan-bridge-cd.yaml b/.github/workflows/pesacheck-meedan-bridge-cd.yaml index ccd554f9..6c7fa336 100644 --- a/.github/workflows/pesacheck-meedan-bridge-cd.yaml +++ b/.github/workflows/pesacheck-meedan-bridge-cd.yaml @@ -6,6 +6,9 @@ name: PesaCheck Meedan Bridge | Continuous Deployment - main paths: - "pesacheck_meedan_bridge/py/VERSION" +# The job only checks out the repo; the deploy uses DockerHub/SSH secrets. +permissions: + contents: read env: DOKKU_REMOTE_URL: "ssh://dokku@dokku-1.dev.codeforafrica.org/pesacheck-meedan-bridge" IMAGE_NAME: "codeforafrica/pesacheck_meedan_bridge" From 54f86bb89e45b900633a6cb60823f0f8326e2494 Mon Sep 17 00:00:00 2001 From: Kevin Koech Date: Thu, 24 Sep 2026 16:20:04 +0300 Subject: [PATCH 10/10] fix(pesacheck_meedan_bridge): Version the User-Agent, point it at PesaCheck Review feedback on #1209. py/VERSION isn't packaged into the pex, so the User-Agent carries its own version rather than reading that file. --- pesacheck_meedan_bridge/py/main.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pesacheck_meedan_bridge/py/main.py b/pesacheck_meedan_bridge/py/main.py index fb2b57ad..9dd6c7c4 100755 --- a/pesacheck_meedan_bridge/py/main.py +++ b/pesacheck_meedan_bridge/py/main.py @@ -30,8 +30,9 @@ def extract_summary(feed): # Identify the bridge instead of defaulting to "python-requests/x.y.z", which -# Cloudflare challenges in front of pesacheck.org. -USER_AGENT = "pesacheck-meedan-bridge (+https://github.com/CodeForAfrica/api)" +# Cloudflare challenges in front of pesacheck.org. Kept separate from +# py/VERSION, which isn't packaged into the pex. +USER_AGENT = "PesaCheckMeedanBridge/1.0 (+https://pesacheck.org)" language_codes = { "english": "en",