AppBuilder Agent #555
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: 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 |