From 9629fdc66dc7acf53711ea7808d4c86f8c130e28 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 18 Jun 2026 16:55:26 -0700 Subject: [PATCH] Add reusable publish-expensidev-image workflow Centralize multi-arch Expensidev Docker image publishing so consumer repos can keep repo-specific triggers while sharing build logic. Co-authored-by: Cursor --- .../workflows/publish-expensidev-image.yml | 127 ++++++++++++++++++ README.md | 60 ++++++++- 2 files changed, 181 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/publish-expensidev-image.yml diff --git a/.github/workflows/publish-expensidev-image.yml b/.github/workflows/publish-expensidev-image.yml new file mode 100644 index 0000000..ef0c31b --- /dev/null +++ b/.github/workflows/publish-expensidev-image.yml @@ -0,0 +1,127 @@ +name: Publish Expensidev Image + +on: + workflow_call: + inputs: + image_name: + description: "Full GHCR image name (for example, ghcr.io/expensify/expensidev-php)" + required: true + type: string + dockerfile: + description: "Dockerfile path relative to the Expensidev repository root (for example, ci/docker/Dockerfile.php)" + required: true + type: string + concurrency_group: + description: "Concurrency group name used by the calling workflow" + required: true + type: string + extra_checkouts: + description: "JSON array of extra repository checkouts. Each object supports name, repository, ref, path, and optional submodules (boolean)." + required: false + type: string + default: "[]" + +env: + IMAGE_NAME: ${{ inputs.image_name }} + CHECKOUT_TOKEN: ${{ secrets.MELVIN_GH_TOKEN || secrets.CODE_EXPENSIFY_TOKEN || github.token }} + +jobs: + build: + name: Build ${{ matrix.arch }} + runs-on: ${{ matrix.runner }} + timeout-minutes: 90 + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + runner: blacksmith-32vcpu-ubuntu-2404 + platform: linux/amd64 + - arch: arm64 + runner: blacksmith-32vcpu-ubuntu-2404-arm + platform: linux/arm64 + steps: + - name: Checkout Expensidev + uses: useblacksmith/checkout@c9796daa2a4bdebdab5bd16be2c09a70cd4e1121 # v1 + with: + repository: Expensify/Expensidev + ref: main + token: ${{ env.CHECKOUT_TOKEN }} + path: Expensidev + + - name: Checkout extra repositories + env: + EXTRA_CHECKOUTS: ${{ inputs.extra_checkouts }} + run: | + set -euo pipefail + + if [ -z "$EXTRA_CHECKOUTS" ] || [ "$EXTRA_CHECKOUTS" = "[]" ]; then + echo "No extra checkouts requested" + exit 0 + fi + + while IFS= read -r checkout; do + name=$(echo "$checkout" | jq -r '.name') + repository=$(echo "$checkout" | jq -r '.repository') + ref=$(echo "$checkout" | jq -r '.ref') + path=$(echo "$checkout" | jq -r '.path') + submodules=$(echo "$checkout" | jq -r '.submodules // false') + + echo "Checking out ${name} (${repository}@${ref}) to ${path}" + mkdir -p "$(dirname "$path")" + + clone_url="https://x-access-token:${CHECKOUT_TOKEN}@github.com/${repository}.git" + if [ "$submodules" = "true" ]; then + git clone --branch "$ref" --recursive "$clone_url" "$path" + else + git clone --branch "$ref" "$clone_url" "$path" + fi + done < <(echo "$EXTRA_CHECKOUTS" | jq -c '.[]') + + - name: Setup Docker Builder + uses: useblacksmith/setup-docker-builder@33fed32c1ba8775f20366ec9e8d0cd9fe8fc1dd3 # v1.3.0 + + - name: Login to GitHub Container Registry + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ env.CHECKOUT_TOKEN }} + + - name: Build and push image + uses: useblacksmith/build-push-action@30c71162f16ea2c27c3e21523255d209b8b538c1 # v2 + with: + context: Expensidev + file: Expensidev/${{ inputs.dockerfile }} + push: true + provenance: false + platforms: ${{ matrix.platform }} + tags: ${{ env.IMAGE_NAME }}:latest-${{ matrix.arch }} + + publish-manifest: + name: Publish multi-arch manifest + runs-on: blacksmith-2vcpu-ubuntu-2404 + needs: [build] + permissions: + contents: read + packages: write + steps: + - name: Setup Docker Builder + uses: useblacksmith/setup-docker-builder@33fed32c1ba8775f20366ec9e8d0cd9fe8fc1dd3 # v1.3.0 + + - name: Login to GitHub Container Registry + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ env.CHECKOUT_TOKEN }} + + - name: Publish latest manifest + run: | + docker buildx imagetools create \ + --tag "${{ env.IMAGE_NAME }}:latest" \ + "${{ env.IMAGE_NAME }}:latest-amd64" \ + "${{ env.IMAGE_NAME }}:latest-arm64" diff --git a/README.md b/README.md index fd85a3d..220a288 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Expensify Shared GitHub Actions workflows 🔄 +# Expensify Shared GitHub Actions workflows 🔄 ## What is the repository used for? @@ -18,7 +18,7 @@ jobs: with: # Repository name with owner. For example, Expensify/eslint-config-expensify # Required, String, default: ${{ github.repository }} - repository: '' + repository: "" # True if we should run npm run build for the package # Optional, Boolean, default: false @@ -37,13 +37,61 @@ jobs: secrets: inherit ``` +### `publish-expensidev-image.yml` + +Builds and publishes multi-arch Expensidev Docker images to GHCR. Consumer repositories keep their own triggers and concurrency settings, then call this reusable workflow with image-specific inputs. + +```yml +on: + push: + branches: + - production + workflow_dispatch: + +concurrency: + group: publish-expensidev-php-image + cancel-in-progress: false + +jobs: + publish: + uses: Expensify/GitHub-Actions/.github/workflows/publish-expensidev-image.yml@main + secrets: inherit + permissions: + contents: read + packages: write + with: + # Full GHCR image name + # Required, String + image_name: ghcr.io/expensify/expensidev-php + + # Dockerfile path relative to the Expensidev repository root + # Required, String + dockerfile: ci/docker/Dockerfile.php + + # Concurrency group name used by the calling workflow + # Required, String + concurrency_group: publish-expensidev-php-image + + # JSON array of extra repository checkouts checked out under Expensidev/ + # Optional, String, default: [] + # Each object supports: name, repository, ref, path, submodules (optional boolean) + extra_checkouts: >- + [ + {"name":"Auth","repository":"Expensify/Auth","ref":"${{ github.sha }}","path":"Expensidev/Auth"}, + {"name":"Bedrock","repository":"Expensify/Bedrock","ref":"expensify_prod","path":"Expensidev/Bedrock","submodules":true} + ] +``` + +The workflow always checks out `Expensify/Expensidev@main`, then applies any `extra_checkouts` before building `linux/amd64` and `linux/arm64` images and publishing a combined `:latest` manifest. + ## Rulesets + GitHub [org-level rulesets](https://docs.github.com/en/enterprise-cloud@latest/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets#require-workflows-to-pass-before-merging) can be configured to run a workflow check against pull requests in all repos in the org. This is a very powerful feature, but there are some caveats and best practices to be aware of when enabling a ruleset. - Supported Event Triggers are documented [here](https://docs.github.com/en/enterprise-cloud@latest/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets#supported-event-triggers). However: - - When a workflow runs in response to a ruleset, some configs such as `branches`, `paths`, `paths-ignore`, that would normally be valid in a workflow are ignored. - - The default activity types for each event will be used. This means that something like `pull_request:comment` will not work - the `pull_request` event will always be triggered for the default activity types listed in the documentation. - - If you need to target or exclude specific branches, that can be configured in the ruleset settings. - - If you need to target or exclude specific paths, that must be implemented manually in the workflow itself. + - When a workflow runs in response to a ruleset, some configs such as `branches`, `paths`, `paths-ignore`, that would normally be valid in a workflow are ignored. + - The default activity types for each event will be used. This means that something like `pull_request:comment` will not work - the `pull_request` event will always be triggered for the default activity types listed in the documentation. + - If you need to target or exclude specific branches, that can be configured in the ruleset settings. + - If you need to target or exclude specific paths, that must be implemented manually in the workflow itself. - Due to a GitHub :bug:, PRs that are open when the rule is enabled will get stuck with a pending check that will never get picked up. The easiest way to fix that is to close and reopen the PR. Consider writing a script to close and reopen all open PRs across the org after the check is enabled. - It is less disruptive to [configure the ruleset to `Evaluate` first](https://docs.github.com/en/enterprise-cloud@latest/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets#using-evaluate-mode-for-ruleset-workflows), then `Active` once the kinks are worked out.