Skip to content

AppBuilder Agent

AppBuilder Agent #548

Workflow file for this run

name: AppBuilder Agent
on:
push:
pull_request:
workflow_dispatch:
inputs:
logLevel:
description: "Log Level"
required: true
default: "warning"
tags:
description: "Tags"
base_image_tag:
description: "Base image tag for app-builders (e.g. feature-xxx, staging, latest)"
required: false
default: ""
schedule:
- cron: "0 0 * * 0" # weekly
env:
BUILD_TAG: build-appbuilder-agent:latest
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Show environment
run: env
- name: Determine docker tags
id: meta
run: |
DOCKER_TAG=latest
if [[ "${{ github.event.inputs.base_image_tag }}" != "" ]]; then
# When custom base image tag is provided, use it as the output tag too
DOCKER_TAG="${{ github.event.inputs.base_image_tag }}"
APP_ENV="stg"
else
# Original logic for automatic builds
if [[ "${GITHUB_HEAD_REF}" != "" ]]; then BRANCH="${GITHUB_HEAD_REF}"; else BRANCH="${GITHUB_REF_NAME}"; fi
case $BRANCH in develop) APP_ENV="stg" ;; master) APP_ENV="prd" ;; *) APP_ENV="stg" && DOCKER_TAG="${BRANCH/\//-}" ;; esac
fi
echo "Branch=${BRANCH:-custom}"
echo "DockerTag=${DOCKER_TAG}"
echo "AppEnv=${APP_ENV}"
echo "Branch=${BRANCH:-custom}" >> $GITHUB_OUTPUT
echo "DockerTag=${DOCKER_TAG}" >> $GITHUB_OUTPUT
echo "AppEnv=${APP_ENV}" >> $GITHUB_OUTPUT
- name: Determine base image tag for app-builders
id: base-image
uses: actions/github-script@v7
with:
script: |
// Check if base image tag was provided via workflow input
const inputTag = '${{ github.event.inputs.base_image_tag }}';
if (inputTag && inputTag.trim() !== '') {
core.setOutput('tag', inputTag.trim());
console.log(`Using workflow input base image tag: ${inputTag.trim()}`);
return;
}
const branch = process.env.BRANCH;
if (branch === 'master') {
core.setOutput('tag', 'latest');
console.log('Using latest tag for master branch');
return;
}
let token = null;
// Request anonymous pull token for ghcr.io
try {
console.log('Requesting anonymous pull token for ghcr.io...');
const tokenRes = await fetch('https://ghcr.io/token?scope=repository:sillsdev/app-builders:pull', {
headers: { 'Accept': 'application/json' }
});
console.log(`Token response: ${tokenRes.status} ${tokenRes.statusText}`);
if (!tokenRes.ok) {
throw new Error(`Failed to get token: ${tokenRes.status} ${tokenRes.statusText}`);
}
const tokenData = await tokenRes.json();
if (!tokenData.token) {
core.setOutput('tag', 'latest');
console.log('No token received in response, defaulting to latest tag');
return;
}
token = tokenData.token;
console.log('Successfully obtained pull token.');
} catch (tokenError) {
console.log('Error obtaining pull token or checking tags with auth:', tokenError);
console.log('Failed to get token, default to latest tag');
core.setOutput('tag', 'latest');
return;
}
// For develop branch, check which of 'staging' or 'latest' is newer in GHCR
const headers = {
'Accept': 'application/vnd.oci.image.manifest.v1+json',
'Authorization': `Bearer ${token}`
};
try {
// Check if staging tag exists in GHCR (public registry, no auth)
console.log('Checking for staging tag in GHCR...');
const stagingRes = await fetch('https://ghcr.io/v2/sillsdev/app-builders/manifests/staging', { headers });
console.log(`Staging tag response: ${stagingRes.status} ${stagingRes.statusText}`);
const stagingExists = stagingRes.status === 200;
// Check if latest tag exists in GHCR (public registry, no auth)
console.log('Checking for latest tag in GHCR...');
const latestRes = await fetch('https://ghcr.io/v2/sillsdev/app-builders/manifests/latest', { headers });
console.log(`Latest tag response: ${latestRes.status} ${latestRes.statusText}`);
const latestExists = latestRes.status === 200;
console.log(`Staging exists: ${stagingExists}, Latest exists: ${latestExists}`);
if (stagingExists && latestExists) {
// Both tags exist, compare their created dates
console.log('Both staging and latest tags found in GHCR, comparing creation dates...');
// Helper to get created timestamp from a tag
async function getCreatedDate(tag) {
console.log(`Fetching manifest for ${tag}...`);
const manifestRes = await fetch(`https://ghcr.io/v2/sillsdev/app-builders/manifests/${tag}`,{ headers });
if (!manifestRes.ok) {
throw new Error(`Failed to fetch manifest for ${tag}: ${manifestRes.status}`);
}
const manifest = await manifestRes.json();
if (!manifest.config || !manifest.config.digest) {
throw new Error(`No config digest found in manifest for ${tag}`);
}
const configDigest = manifest.config.digest;
console.log(`Fetching config blob for ${tag}: ${configDigest}`);
const configRes = await fetch(
`https://ghcr.io/v2/sillsdev/app-builders/blobs/${configDigest}`,
{ headers: { Authorization: `Bearer ${token}` } }
);
if (!configRes.ok) {
throw new Error(`Failed to fetch config blob for ${tag}: ${configRes.status}`);
}
const config = await configRes.json();
if (!config.created) {
throw new Error(`No created timestamp found in config for ${tag}`);
}
return config.created;
}
try {
const stagingCreated = await getCreatedDate('staging');
const latestCreated = await getCreatedDate('latest');
console.log(`staging created: ${stagingCreated}`);
console.log(`latest created: ${latestCreated}`);
// ISO timestamps sort lexicographically
if (stagingCreated > latestCreated) {
console.log('Staging image is newer — selecting staging');
core.setOutput('tag', 'staging');
return;
}
if (latestCreated > stagingCreated) {
console.log('Latest image is newer — selecting latest');
core.setOutput('tag', 'latest');
return;
}
console.log('Created timestamps identical — falling through to default behavior');
} catch (compareError) {
console.log('Failed to compare image creation dates:', compareError);
console.log('Falling through to default staging preference');
}
console.log('Both staging and latest exist in GHCR, preferring staging');
core.setOutput('tag', 'staging');
} else if (stagingExists) {
console.log('Only staging tag found in GHCR, using staging');
core.setOutput('tag', 'staging');
} else {
console.log('Using latest tag (staging not found in GHCR)');
core.setOutput('tag', 'latest');
}
} catch (error) {
console.log('Error querying GHCR tags, defaulting to latest:', error);
core.setOutput('tag', 'latest');
}
env:
BRANCH: ${{ steps.meta.outputs.Branch }}
- name: Extract versions from builder image
id: versions
run: |
# Pull the builder image and extract versions.json
BASE_IMAGE_TAG="${{ steps.base-image.outputs.tag }}"
echo "Using app-builders tag: ${BASE_IMAGE_TAG}"
docker pull ghcr.io/sillsdev/app-builders:${BASE_IMAGE_TAG}
CONTAINER_ID=$(docker create ghcr.io/sillsdev/app-builders:${BASE_IMAGE_TAG} /bin/true)
docker cp ${CONTAINER_ID}:/versions.json ./versions.json
docker rm ${CONTAINER_ID}
# Parse versions.json using jq and set outputs
echo "Contents of versions.json:"
cat ./versions.json
VERSION_SAB=$(jq -r '.scriptureappbuilder' ./versions.json)
VERSION_RAB=$(jq -r '.readingappbuilder' ./versions.json)
VERSION_DAB=$(jq -r '.dictionaryappbuilder' ./versions.json)
VERSION_KAB=$(jq -r '.keyboardappbuilder' ./versions.json)
echo "Extracted versions:"
echo " scriptureappbuilder: $VERSION_SAB"
echo " readingappbuilder: $VERSION_RAB"
echo " dictionaryappbuilder: $VERSION_DAB"
echo " keyboardappbuilder: $VERSION_KAB"
echo "VERSION_SAB=$VERSION_SAB" >> $GITHUB_OUTPUT
echo "VERSION_RAB=$VERSION_RAB" >> $GITHUB_OUTPUT
echo "VERSION_DAB=$VERSION_DAB" >> $GITHUB_OUTPUT
echo "VERSION_KAB=$VERSION_KAB" >> $GITHUB_OUTPUT
- name: Build docker image
uses: docker/build-push-action@v6
with:
context: .
load: true
tags: ${{ env.BUILD_TAG }}
build-args: |
BASE_IMAGE_TAG=${{ steps.base-image.outputs.tag }}
VERSION_SAB=${{ steps.versions.outputs.VERSION_SAB }}
VERSION_RAB=${{ steps.versions.outputs.VERSION_RAB }}
VERSION_DAB=${{ steps.versions.outputs.VERSION_DAB }}
VERSION_KAB=${{ steps.versions.outputs.VERSION_KAB }}
- name: Get version
id: version
run: |
docker images
mkdir $HOME/out
id=$(docker create ${{ env.BUILD_TAG }})
docker cp $id:/app-builders/VERSION $HOME/out
docker rm -v $id
echo "VersionTag=$(cat $HOME/out/VERSION)" >> $GITHUB_OUTPUT
- name: Configure AWS credentials (SIL)
if: ${{ github.event.inputs.base_image_tag == '' }}
id: aws_sil
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.SIL__AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.SIL__AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.SIL__AWS_DEFAULT_REGION }}
- name: Login to AWS ECR (SIL)
if: ${{ github.event.inputs.base_image_tag == '' }}
id: ecr_sil
uses: aws-actions/amazon-ecr-login@v2
with:
registries: ${{ secrets.SIL__AWS_ECR_ACCOUNT }}
- name: Push to AWS ECR (SIL)
if: ${{ github.event.inputs.base_image_tag == '' }}
run: |
docker tag ${{ env.BUILD_TAG }} "${{ steps.ecr_sil.outputs.registry }}/appbuilder-agent-${{ steps.meta.outputs.AppEnv }}:${{ steps.meta.outputs.DockerTag }}"
docker tag ${{ env.BUILD_TAG }} "${{ steps.ecr_sil.outputs.registry }}/appbuilder-agent-${{ steps.meta.outputs.AppEnv }}:${{ steps.version.outputs.VersionTag }}"
docker push "${{ steps.ecr_sil.outputs.registry }}/appbuilder-agent-${{ steps.meta.outputs.AppEnv }}:${{ steps.meta.outputs.DockerTag }}"
docker push "${{ steps.ecr_sil.outputs.registry }}/appbuilder-agent-${{ steps.meta.outputs.AppEnv }}:${{ steps.version.outputs.VersionTag }}"
- name: Configure AWS credentials (FCBH)
if: ${{ steps.meta.outputs.AppEnv == 'prd' && github.event.inputs.base_image_tag == '' }}
id: aws_fcbh
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.FCBH__AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.FCBH__AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.FCBH__AWS_DEFAULT_REGION }}
- name: Login to AWS ECR (FCBH)
if: ${{ steps.meta.outputs.AppEnv == 'prd' && github.event.inputs.base_image_tag == '' }}
id: ecr_fcbh
uses: aws-actions/amazon-ecr-login@v2
with:
registries: ${{ secrets.FCBH__AWS_ECR_ACCOUNT }}
- name: Push to AWS ECR (FCBH)
if: ${{ steps.meta.outputs.AppEnv == 'prd' && github.event.inputs.base_image_tag == '' }}
run: |
docker tag ${{ env.BUILD_TAG }} "${{ steps.ecr_fcbh.outputs.registry }}/appbuilder-agent-${{ steps.meta.outputs.AppEnv }}:${{ steps.meta.outputs.DockerTag }}"
docker tag ${{ env.BUILD_TAG }} "${{ steps.ecr_fcbh.outputs.registry }}/appbuilder-agent-${{ steps.meta.outputs.AppEnv }}:${{ steps.version.outputs.VersionTag }}"
docker push "${{ steps.ecr_fcbh.outputs.registry }}/appbuilder-agent-${{ steps.meta.outputs.AppEnv }}:${{ steps.meta.outputs.DockerTag }}"
docker push "${{ steps.ecr_fcbh.outputs.registry }}/appbuilder-agent-${{ steps.meta.outputs.AppEnv }}:${{ steps.version.outputs.VersionTag }}"
- name: Configure AWS credentials (LU)
if: ${{ steps.meta.outputs.AppEnv == 'stg' && github.event.inputs.base_image_tag == '' }}
id: aws_lu
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.LU__AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.LU__AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.LU__AWS_DEFAULT_REGION }}
- name: Login to AWS ECR (LU)
if: ${{ steps.meta.outputs.AppEnv == 'stg' && github.event.inputs.base_image_tag == '' }}
id: ecr_lu
uses: aws-actions/amazon-ecr-login@v2
with:
registries: ${{ secrets.LU__AWS_ECR_ACCOUNT }}
- name: Push to AWS ECR (LU)
if: ${{ steps.meta.outputs.AppEnv == 'stg' && github.event.inputs.base_image_tag == '' }}
run: |
docker tag ${{ env.BUILD_TAG }} "${{ steps.ecr_lu.outputs.registry }}/appbuilder-agent-${{ steps.meta.outputs.AppEnv }}:${{ steps.meta.outputs.DockerTag }}"
docker tag ${{ env.BUILD_TAG }} "${{ steps.ecr_lu.outputs.registry }}/appbuilder-agent-${{ steps.meta.outputs.AppEnv }}:${{ steps.version.outputs.VersionTag }}"
docker push "${{ steps.ecr_lu.outputs.registry }}/appbuilder-agent-${{ steps.meta.outputs.AppEnv }}:${{ steps.meta.outputs.DockerTag }}"
docker push "${{ steps.ecr_lu.outputs.registry }}/appbuilder-agent-${{ steps.meta.outputs.AppEnv }}:${{ steps.version.outputs.VersionTag }}"
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push to GHCR
run: |
docker tag ${{ env.BUILD_TAG }} "ghcr.io/sillsdev/appbuilder-agent-${{ steps.meta.outputs.AppEnv }}:${{ steps.meta.outputs.DockerTag }}"
docker push "ghcr.io/sillsdev/appbuilder-agent-${{ steps.meta.outputs.AppEnv }}:${{ steps.meta.outputs.DockerTag }}"
# Only push version tag for automatic builds (when base_image_tag is not specified)
if [[ "${{ github.event.inputs.base_image_tag }}" == "" ]]; then
docker tag ${{ env.BUILD_TAG }} "ghcr.io/sillsdev/appbuilder-agent-${{ steps.meta.outputs.AppEnv }}:${{ steps.version.outputs.VersionTag }}"
docker push "ghcr.io/sillsdev/appbuilder-agent-${{ steps.meta.outputs.AppEnv }}:${{ steps.version.outputs.VersionTag }}"
fi
- name: Cleanup older untagged packages, keep 2
uses: actions/delete-package-versions@v5
with:
package-name: "appbuilder-agent-${{ steps.meta.outputs.AppEnv }}"
package-type: "container"
min-versions-to-keep: 2
delete-only-untagged-versions: "true"
- name: Cleanup older all packages, keep 6
uses: actions/delete-package-versions@v5
with:
package-name: "appbuilder-agent-${{ steps.meta.outputs.AppEnv }}"
package-type: "container"
min-versions-to-keep: 6