This document describes the current workflow setup for shared core images and reusable downstream app images.
Workflow roles
The current workflow layout is:
.github/workflows/core-build-develop.yml.github/workflows/core-build-stable.yml.github/workflows/core-build-test-images.yml.github/workflows/core-publish-images.yml.github/workflows/app-build-image.yml
core-build-develop.yml and core-build-stable.yml are orchestration workflows. They decide when the core image pipeline runs.
core-build-test-images.yml is the reusable workflow that:
- resolves the image versions for the requested release line
- builds the shared core images into a local registry
- runs the test suite against those images
core-publish-images.yml is the reusable workflow that:
- publishes the tested images to Docker Hub
- publishes
baseandbuildto GHCR
app-build-image.yml is the reusable workflow that downstream repositories call to:
- create an
apps.jsonfile from the caller's app repository and ref - build
images/layered/Containerfile - consume existing
baseandbuildimages - install the requested app into the final image
- optionally push the final app image to the caller's registry
Current flow
The current structure is:
core orchestration
-> core build and test
-> core publish
downstream app workflow
-> consume published base and build
-> install app
-> publish final app imageCurrent Mermaid overview:
flowchart TD
subgraph Core["Core image flow"]
A[core-build-develop.yml or core-build-stable.yml]
B[core-build-test-images.yml]
C[Resolve versions]
D[Build local test images]
E[Run pytest]
F[core-publish-images.yml]
G[Push Docker Hub: erpnext, base, build]
H[Push GHCR: base, build]
A --> B
B --> C
C --> D
D --> E
E --> F
F --> G
F --> H
end
subgraph App["Downstream app flow"]
I[Downstream repo workflow]
J[app-build-image.yml]
K[Create apps.json]
L[Build images/layered/Containerfile]
M[Install app]
N[Push final app image]
I --> J
J --> K
K --> L
L --> M
M --> N
end
G --> J
H --> JMore concretely:
core-build-test-images.yml
-> resolves frappe and erpnext tags
-> builds images into a local CI registry
-> runs tests
core-publish-images.yml
-> pushes Docker Hub: erpnext, base, build
-> pushes GHCR: base, build
app-build-image.yml
-> pulls:
- <prefix>/base:<frappe_ref>
- <prefix>/build:<frappe_ref>
-> installs app from app_repo + app_ref
-> pushes final image_name:image_tagNaming convention
GitHub Actions requires workflow files to stay directly inside .github/workflows. Subdirectories are not supported for workflow files, so structure should come from file names and name: values.
Recommended file naming convention:
<area>-<action>-<subject>.ymlCurrent examples:
core-build-bench.ymlcore-build-develop.ymlcore-build-stable.ymlcore-build-test-images.ymlcore-publish-images.ymlapp-build-image.ymldocs-publish-site.yml
Recommended visible workflow names:
Core / Build BenchCore / Build DevelopCore / Build StableCore / Build and Test ImagesCore / Publish ImagesApp / Build ImageDocs / Publish Site
Style rules
To keep workflows predictable, use one convention per category instead of mixing styles.
Workflow file names should use kebab-case:
core-build-test-images.yml
app-build-image.ymlWorkflow display names should use short title-style groups:
Core / Build and Test Images
App / Build ImageWorkflow inputs should use snake_case:
app_name:
frappe_ref:
image_name:Environment variables should use upper snake case:
FRAPPE_IMAGE_PREFIX
PYTHON_VERSION
NODE_VERSIONThe recommended rule set is:
- workflow file names: kebab-case
- workflow
name:values: grouped title case - workflow inputs: snake_case
- job ids and step ids: snake_case where practical
- environment variables: UPPER_SNAKE_CASE
This means - is preferred for file names, while _ remains appropriate for YAML keys, inputs, and environment variables.
Important inputs in app-build-image.yml
The reusable app workflow is controlled mainly by these inputs:
app_nameThe application directory name, for examplecrmapp_repoThe Git repository to install, for examplefrappe/crmapp_refThe branch or tag to install, for exampledevelopfrappe_refThe tag of the existingbaseandbuildimages, for exampleversion-16frappe_image_prefixWhere the sharedbaseandbuildimages come from, for examplefrappeorghcr.io/frappeimage_nameThe final target image name, for exampleghcr.io/acme/crmimage_tagThe final target image tag, for exampledevelopregistryThe registry for the final push, for exampleghcr.ioordocker.io
The key distinction is:
frappe_image_prefix = source of shared base/build images
image_name = destination of the final app imageExample: caller repository publishes to GHCR
This example assumes:
- shared base images exist in
ghcr.io/frappe/baseandghcr.io/frappe/build - the caller repository wants to publish its own app image to
ghcr.io/acme/crm
name: App / Build CRM Image
on:
workflow_dispatch:
push:
branches:
- develop
permissions:
contents: read
packages: write
jobs:
build-image:
uses: frappe/frappe_docker/.github/workflows/app-build-image.yml@main
with:
app_name: crm
app_repo: acme/crm
app_ref: develop
frappe_ref: version-16
frappe_image_prefix: ghcr.io/frappe
image_name: ghcr.io/acme/crm
image_tag: develop
registry: ghcr.io
push: true
platforms: linux/amd64What happens:
1. app-build-image.yml is called
2. apps.json is generated from acme/crm + develop
3. the workflow builds images/layered/Containerfile
4. layered uses:
- ghcr.io/frappe/build:version-16
- ghcr.io/frappe/base:version-16
5. CRM is installed
6. the final image is pushed to ghcr.io/acme/crm:developFor GHCR, the caller workflow should grant:
permissions: packages: write
The reusable workflow then logs in with the workflow token.
Example: caller repository publishes to Docker Hub
This example assumes:
- shared base images come from Docker Hub under
frappe - the caller repository wants to publish its app image to Docker Hub as
acme/crm
name: App / Build CRM Image
on:
workflow_dispatch:
push:
branches:
- develop
jobs:
build-image:
uses: frappe/frappe_docker/.github/workflows/app-build-image.yml@main
with:
app_name: crm
app_repo: acme/crm
app_ref: develop
frappe_ref: version-16
frappe_image_prefix: frappe
image_name: acme/crm
image_tag: develop
registry: docker.io
push: true
platforms: linux/amd64
secrets:
REGISTRY_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
REGISTRY_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}In this case:
- shared images are pulled from
frappe/base:version-16andfrappe/build:version-16 - the final image is pushed to Docker Hub as
acme/crm:develop
