CI Pipeline #526
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |