Skip to content

CI Pipeline

CI Pipeline #526

Workflow file for this run

name: CI Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ '*' ]
schedule:
# Every 6 hours at :05 (00:05 / 06:05 / 12:05 / 18:05 UTC) — poll for
# new podcast episodes and newsletters. The 5-minute offset avoids
# racing content published right on the hour boundary.
- cron: '5 */6 * * *'
workflow_dispatch:
inputs:
automate_content:
description: "Set automate_content to trigger automate-content job"
required: false
default: ""
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
code-changed: ${{ steps.filter.outputs.code }}
image-digest: ${{ steps.image.outputs.digest }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Detect code changes
uses: dorny/paths-filter@v4
id: filter
with:
# Anything that affects the compiled binary or the CI logic itself.
# Content/ markdown is runtime data and is intentionally excluded.
filters: |
code:
- 'Sources/**'
- 'Package.swift'
- 'Package.resolved'
- '.swift-version'
- 'Dockerfile'
- '.github/workflows/main.yaml'
- name: Resolve build image digest
id: image
run: |
DIGEST=$(docker buildx imagetools inspect brightdigit/publish-xml:6.4 \
--format '{{.Manifest.Digest}}')
echo "Resolved brightdigit/publish-xml:6.4 -> ${DIGEST}"
echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT"
automate-content:
if: ${{ github.event_name == 'schedule' || inputs.automate_content != '' }}
needs: detect-changes
runs-on: ubuntu-latest
container:
image: brightdigit/publish-xml:6.4
concurrency:
group: automate-content
cancel-in-progress: true
steps:
# The push that lands imported content is authored by an SSH deploy
# key (NOT GITHUB_TOKEN), so it DOES trigger CI -> the fast-deploy
# path. The Swift container is minimal, so install an SSH client first.
- name: Install SSH client
run: apt-get update && apt-get install -y openssh-client
- name: Checkout code
uses: actions/checkout@v6
with:
# Repo deploy key (write access); actions/checkout configures it
# for the later git push so that push triggers a new CI run.
ssh-key: ${{ secrets.CONTENT_DEPLOY_KEY }}
fetch-depth: 0
- name: Mark workspace safe for git
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Capture Swift version
id: swift
run: echo "ver=$(swift --version | head -n1 | tr -cd '[:alnum:].')" >> "$GITHUB_OUTPUT"
- name: Cache SwiftPM Dependencies
uses: actions/cache@v5
with:
path: .build/
key: ${{ runner.os }}-release-${{ steps.swift.outputs.ver }}-${{ needs.detect-changes.outputs.image-digest }}-${{ hashFiles('Package.resolved', 'Sources/**/*.swift') }}
# Same key as package-linux, so the two jobs share the prebuilt binary.
# The image digest keeps a stale-ABI binary from being reused (issue #65).
- name: Cache Release Binary
id: bincache
uses: actions/cache@v5
with:
path: brightdigitwg-Linux-x86_64
key: bin-${{ runner.os }}-${{ steps.swift.outputs.ver }}-${{ needs.detect-changes.outputs.image-digest }}-${{ hashFiles('Package.resolved', 'Package.swift', 'Sources/**/*.swift') }}
- name: Build Release Product
if: ${{ steps.bincache.outputs.cache-hit != 'true' }}
run: |
swift build -c release --product brightdigitwg
BIN_PATH=$(swift build -c release --product brightdigitwg --show-bin-path)
cp "$BIN_PATH/brightdigitwg" "brightdigitwg-$(uname)-$(arch)"
- name: Import Mailchimp
env:
MAILCHIMP_API_KEY: ${{ secrets.MAILCHIMP_API_KEY }}
MAILCHIMP_LIST_ID: ${{ secrets.MAILCHIMP_LIST_ID }}
run: |
echo "Importing Mailchimp..."
chmod +x ./brightdigitwg-Linux-x86_64
./brightdigitwg-Linux-x86_64 \
import mailchimp \
--mailchimp-api-key=$MAILCHIMP_API_KEY \
--mailchimp-list-id=$MAILCHIMP_LIST_ID \
--export-markdown-directory=Content/newsletters
echo "Mailchimp import finished."
- name: Import Podcast
env:
YOUTUBE_API_KEY: ${{ secrets.YOUTUBE_API_KEY }}
run: |
echo "Importing Podcast..."
./brightdigitwg-Linux-x86_64 \
import podcast \
--youtube-api-key=$YOUTUBE_API_KEY \
--export-markdown-directory Content/episodes
echo "Podcast import finished."
- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Check Changes, Commit, and Push
run: |
# Only Content/ is committed; the prebuilt binary and .build/
# stay out of the bot commit by construction.
# POSIX `[` (not bash `[[`): this step runs under the container's
# /bin/sh (dash), where `[[` errors with "[[: not found" and the
# test silently fails to the else branch — so a real change would
# never be committed.
if [ -n "$(git status --porcelain Content/)" ]; then
echo "Changes detected. Proceeding with commit and push."
TODAY_DATE=$(date '+%y%m%d%H%M')
CHANGES_COUNT=$(git status --porcelain Content/ | wc -l | xargs)
COMMIT_MESSAGE="Bot:- ${TODAY_DATE} - ${CHANGES_COUNT} new changes"
git add Content/
git commit -m "${COMMIT_MESSAGE}"
# Pushed over SSH with the deploy key (configured by
# actions/checkout), so this commit DOES trigger CI -> deploy.
git push origin ${{ github.ref_name }}
echo "Changes pushed successfully."
else
echo "No changes detected. Nothing to commit or push."
fi
build-linux:
needs: detect-changes
if: ${{ needs.detect-changes.outputs.code-changed == 'true' }}
runs-on: ubuntu-latest
container:
image: brightdigit/publish-xml:6.4
concurrency:
group: build-${{ github.ref }}
cancel-in-progress: true
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Build and Test
uses: brightdigit/swift-build@v1
lint:
needs: [detect-changes, build-linux]
if: ${{ needs.detect-changes.outputs.code-changed == 'true' }}
runs-on: ubuntu-latest
container:
image: brightdigit/publish-xml:6.4
concurrency:
group: lint-${{ github.ref }}
cancel-in-progress: true
steps:
- name: Checkout code
uses: actions/checkout@v6
# mise-action installs mise and the .mise.toml tools with its own cache
# (swift-format is built from source via the spm backend, ~11 min).
- uses: jdx/mise-action@v4
env:
# Raise the default 30s download timeout: GitHub throttles the SwiftLint
# release zip when fetched under load. Authenticate via the token to lift
# the unauthenticated rate limit.
MISE_HTTP_TIMEOUT: 300s
MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
cache: true
- name: Lint
run: ./Scripts/lint.sh
env:
LINT_MODE: STRICT
package-linux:
needs: [detect-changes, build-linux]
# build-linux is skipped on content-only commits; still run as long as it
# didn't actually fail (success or skipped both proceed).
if: ${{ !cancelled() && needs.build-linux.result != 'failure' }}
runs-on: ubuntu-latest
container:
image: brightdigit/publish-xml:6.4
concurrency:
group: package-${{ github.ref }}
cancel-in-progress: true
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Capture Swift version
id: swift
run: echo "ver=$(swift --version | head -n1 | tr -cd '[:alnum:].')" >> "$GITHUB_OUTPUT"
- name: Cache SwiftPM Dependencies
uses: actions/cache@v5
with:
path: .build/
key: ${{ runner.os }}-release-${{ steps.swift.outputs.ver }}-${{ needs.detect-changes.outputs.image-digest }}-${{ hashFiles('Package.resolved', 'Sources/**/*.swift') }}
# Cache the prebuilt release binary keyed on Swift source state AND the build
# image digest. The digest is essential: keying on source alone deployed an
# ABI-incompatible binary when the image drifted (issue #65 segfault). A
# content-only commit hits this cache and skips the release build entirely.
- name: Cache Release Binary
id: bincache
uses: actions/cache@v5
with:
path: brightdigitwg-Linux-x86_64
key: bin-${{ runner.os }}-${{ steps.swift.outputs.ver }}-${{ needs.detect-changes.outputs.image-digest }}-${{ hashFiles('Package.resolved', 'Package.swift', 'Sources/**/*.swift') }}
- name: Build Release Product
if: ${{ steps.bincache.outputs.cache-hit != 'true' }}
run: |
swift build -c release --product brightdigitwg
BIN_PATH=$(swift build -c release --product brightdigitwg --show-bin-path)
cp "$BIN_PATH/brightdigitwg" "brightdigitwg-$(uname)-$(arch)"
- name: Upload Artifact
uses: actions/upload-artifact@v7
with:
name: brightdigitwg-binaries
path: brightdigitwg-*
# build-macos:
# runs-on: [self-hosted, macOS]
# concurrency:
# group: build-macos
# cancel-in-progress: true
# steps:
# - name: Checkout code
# uses: actions/checkout@v4
# - name: Cache SwiftPM Dependencies
# uses: actions/cache@v4
# with:
# path: .build/
# key: ${{ runner.os }}-${{ hashFiles('Package.resolved') }}
# - name: Build with Swift
# run: swift build
# - name: Run Tests with Swift
# run: swift test
# package-macos:
# needs: build-macos
# runs-on: [self-hosted, macOS]
# concurrency:
# group: build
# cancel-in-progress: true
# steps:
# - name: Checkout code
# uses: actions/checkout@v4
# - name: Cache SwiftPM Dependencies
# uses: actions/cache@v4
# with:
# path: .build/
# key: ${{ runner.os }}-${{ hashFiles('Package.resolved') }}
# - name: Build Release Product
# run: |
# swift build -c release --product brightdigitwg
# BIN_PATH=$(swift build -c release --product brightdigitwg --show-bin-path)
# cp "$BIN_PATH/brightdigitwg" "brightdigitwg-$(uname)-$(arch)"
# - name: Upload Artifact
# uses: actions/upload-artifact@v4
# with:
# name: brightdigitwg-binaries
# path: brightdigitwg-*
deploy:
needs: package-linux
# package-linux survives build-linux being skipped (content-/packages-only
# commits) via its own `if`, but `deploy` had only the implicit `if:
# success()`, which propagates the skipped build-linux down the needs graph
# and skipped deploy too — so bot content pushes built a binary but never
# deployed. An explicit status-function `if` breaks that propagation and
# deploys whenever package-linux actually succeeded.
if: ${{ !cancelled() && needs.package-linux.result == 'success' }}
runs-on: ubuntu-latest
container:
image: brightdigit/publish-xml:6.4
concurrency:
# Scope per-ref so a newer push to the same branch supersedes an
# in-flight deploy, but deploys on different branches/PRs do NOT cancel
# each other (issue #95: a global `deploy` group made passing PRs show a
# false red ✗ when a cross-branch deploy cancelled them).
group: deploy-${{ github.ref }}
cancel-in-progress: true
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Download Artifact
uses: actions/download-artifact@v8
with:
name: brightdigitwg-binaries
- name: Set Publishing Mode and Flags
id: set-publishing
run: |
if [ "${{ github.ref_name }}" = "main" ]; then
echo "PUBLISHING_MODE=production" >> $GITHUB_ENV
echo "PROD_FLAG=--prod" >> $GITHUB_ENV
else
echo "PUBLISHING_MODE=drafts" >> $GITHUB_ENV
echo "PROD_FLAG=" >> $GITHUB_ENV
fi
- name: Run Brightdigitwg
run: |
chmod +x "brightdigitwg-$(uname)-$(arch)"
./brightdigitwg-$(uname)-$(arch) publish --mode $PUBLISHING_MODE
- name: Deploy to Netlify
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_PRODUCTION_SITE_ID: ${{ secrets.NETLIFY_PRODUCTION_SITE_ID }}
run: |
netlify deploy --site $NETLIFY_PRODUCTION_SITE_ID --auth $NETLIFY_AUTH_TOKEN $PROD_FLAG