From e2c2fc6f22f9be777dd093f60b3bc95278222d29 Mon Sep 17 00:00:00 2001 From: Kyle Harding Date: Sat, 10 Feb 2024 13:18:31 -0500 Subject: [PATCH] Create workflow to build and deploy balenaOS Change-type: minor Signed-off-by: Kyle Harding --- .../workflows/dispatch-yocto-build-deploy.yml | 124 +++ .github/workflows/yocto-build-deploy.yml | 732 ++++++++++++++++++ automation/Dockerfile_s3-deploy-env | 13 + 3 files changed, 869 insertions(+) create mode 100644 .github/workflows/dispatch-yocto-build-deploy.yml create mode 100644 .github/workflows/yocto-build-deploy.yml create mode 100644 automation/Dockerfile_s3-deploy-env diff --git a/.github/workflows/dispatch-yocto-build-deploy.yml b/.github/workflows/dispatch-yocto-build-deploy.yml new file mode 100644 index 000000000..3f2c656ac --- /dev/null +++ b/.github/workflows/dispatch-yocto-build-deploy.yml @@ -0,0 +1,124 @@ +name: "Dispatch Yocto" + +on: + pull_request: + types: [opened, synchronize, closed] + branches: + - "main" + - "master" + pull_request_target: + types: [opened, synchronize, closed] + branches: + - "main" + - "master" + + workflow_dispatch: + # you may only define up to 10 `inputs` for a `workflow_dispatch` event + inputs: + device-repo: # only required when testing from non-device repositories + description: balenaOS device repository (owner/repo) + required: true + type: string + default: balena-os/balena-generic + device-repo-ref: # only required when testing from non-device repositories + description: balenaOS device repository tag, branch, or commit to build + required: false + type: string + default: master + meta-balena-ref: + description: meta-balena ref if not the currently pinned version + required: false + type: string + yocto-scripts-ref: + description: balena-yocto-scripts ref if not the currently pinned version + required: false + type: string + machine: + description: yocto board name + required: true + type: string + default: generic-amd64 + # environment: + # description: The GitHub Environment to use for the job(s) (production, staging, etc.) + # required: true + # type: choice + # options: + # - production + # - staging + environment: # TODO: remove this input once the above is enabled + description: Select deploy environment + required: false + type: choice + options: + - > + { + "environment": "balena-staging.com", + "s3-bucket": "resin-staging-img", + "s3-region": "us-east-1", + "aws-subnet": "subnet-0d73c1f0da85add17", + "aws-security-group": "sg-09dd285d11b681946" + } + - > + { + "environment": "balena-cloud.com", + "s3-bucket": "resin-production-img-cloudformation", + "s3-region": "us-east-1", + "aws-subnet": "subnet-02d18a08ea4058574", + "aws-security-group": "sg-057937f4d89d9d51c" + } + deploy-s3: + description: Whether to deploy images to S3 + required: false + type: boolean + default: true + deploy-hostapp: + description: Whether to deploy a hostApp container image to a balena environment + required: false + type: boolean + default: true + deploy-ami: # TODO: can we get this from a source of truth like contracts? + description: Whether to deploy an AMI to AWS + required: false + type: boolean + default: false + sign-image: # TODO: can we get this from a source of truth like contracts? + description: Whether to sign image for secure boot + required: false + type: boolean + default: false + # os-dev: + # description: Enable OS development features + # required: false + # type: boolean + # default: false + # deploy-esr: + # description: Enable to deploy ESR + # required: false + # type: boolean + # default: false + +jobs: + yocto-build-deploy: + name: Yocto + uses: ./.github/workflows/yocto-build-deploy.yml + secrets: inherit + with: + runs-on: '[ "ubuntu-latest" ]' + device-repo: ${{ inputs.device-repo || 'balena-os/balena-generic' }} + device-repo-ref: ${{ inputs.device-repo-ref || 'master' }} + meta-balena-ref: ${{ inputs.meta-balena-ref }} + yocto-scripts-ref: ${{ inputs.yocto-scripts-ref }} + machine: ${{ inputs.machine || 'generic-amd64' }} + # TODO: use environment to inherit balena-url, s3-region, s3-bucket, aws-subnet, aws-security-group + # environment: ${{ inputs.environment }} + balena-url: ${{ fromJSON(inputs.environment || '{}').environment || 'balena-staging.com' }} # removeme + s3-region: ${{ fromJSON(inputs.environment || '{}').s3-region || 'us-east-1' }} # removeme + s3-bucket: ${{ fromJSON(inputs.environment || '{}').s3-bucket || 'resin-staging-img' }} # removeme + aws-subnet: ${{ fromJSON(inputs.environment || '{}').aws-subnet || 'subnet-0d73c1f0da85add17' }} # removeme + aws-security-group: ${{ fromJSON(inputs.environment || '{}').aws-security-group || 'sg-09dd285d11b681946' }} # removeme + deploy-s3: ${{ inputs.deploy-s3 || false }} + deploy-hostapp: ${{ inputs.deploy-hostapp || false }} + deploy-ami: ${{ inputs.deploy-ami || false }} + sign-image: ${{ inputs.sign-image || false }} + os-dev: ${{ inputs.os-dev || false }} + deploy-esr: ${{ inputs.deploy-esr || false }} diff --git a/.github/workflows/yocto-build-deploy.yml b/.github/workflows/yocto-build-deploy.yml new file mode 100644 index 000000000..6862b53c6 --- /dev/null +++ b/.github/workflows/yocto-build-deploy.yml @@ -0,0 +1,732 @@ +name: "Yocto" + +on: + workflow_call: + secrets: + BALENA_API_KEY: + description: balena API key for deploying releases + required: false + S3_ACCESS_KEY: + description: S3 access key + required: false + S3_SECRET_KEY: + description: S3 secret key + required: false + DOCKERHUB_USER: + description: Dockerhub user for pulling private helper images + required: false + DOCKERHUB_TOKEN: + description: Dockerhub token for pulling private helper images + required: false + SIGN_KMOD_KEY_APPEND: + description: Base64-encoded public key of a kernel module signing keypair + required: false + # TODO: can this be the same as BALENA_API_KEY? + SIGN_API_KEY: + description: balena API key that provides access to the signing server + required: false + inputs: + runs-on: + description: The runner labels to use for the job(s) + required: false + type: string + default: > + [ + "self-hosted", + "X64" + ] + device-repo: + description: balenaOS device repository (owner/repo) + required: false + type: string + default: ${{ github.repository }} + device-repo-ref: + description: balenaOS device repository tag, branch, or commit to build + required: false + type: string + default: ${{ github.ref }} + meta-balena-ref: + description: meta-balena ref if not the currently pinned version + required: false + type: string + yocto-scripts-ref: + description: balena-yocto-scripts ref if not the currently pinned version + required: false + type: string + machine: + description: yocto board name + required: true + type: string + # the GitHub Environment will be pre-populated with all the vars required for the job + environment: + description: The GitHub Environment to use for the job(s) (production, staging, etc.) + required: false + type: string + default: staging + # TODO: remove this input and read var directly from GitHub Environment into job.env + balena-url: + description: balena environment URL (e.g. balena-cloud.com) + required: false + type: string + default: ${{ vars.BALENA_URL || 'balena-cloud.com' }} + # TODO: remove this input and read var directly from GitHub Environment into job.env + hostapp-org: + description: balenaCloud Organization for deploying hostapp releases + required: false + type: string + default: ${{ vars.HOSTAPP_ORG || 'balena_os' }} + # TODO: remove this input and read var directly from GitHub Environment into job.env + s3-region: + description: S3 region (e.g. 'us-east-1') + required: false + type: string + default: ${{ vars.S3_REGION || 'us-east-1' }} + # TODO: remove this input and read var directly from GitHub Environment into job.env + s3-bucket: + description: S3 bucket (e.g. resin-production-img-cloudformation) + required: false + type: string + default: ${{ vars.S3_BUCKET || 'resin-production-img-cloudformation' }} + # TODO: remove this input and read var directly from GitHub Environment into job.env + aws-subnet: + description: AWS subnet for AMI deploys + required: false + type: string + default: ${{ vars.AWS_SUBNET || 'subnet-02d18a08ea4058574' }} + # TODO: remove this input and read var directly from GitHub Environment into job.env + aws-security-group: + description: AWS security group for AMI deploys + required: false + type: string + default: ${{ vars.AWS_SECURITY_GROUP || 'sg-057937f4d89d9d51c' }} + # TODO: remove this input and read var directly from GitHub Environment into job.env + sign-api-url: + description: URL for signing secure-boot images + required: false + type: string + default: ${{ vars.SIGN_API_URL || 'https://sign.balena-cloud.com' }} + # TODO: remove this input and read var directly from GitHub Environment into job.env + yocto-cache-host: + description: Yocto NFS sstate cache host + required: false + type: string + default: ${{ vars.YOCTO_CACHE_HOST || 'nfs.product-os.io' }} + # TODO: remove this input and read var directly from GitHub Environment into job.env + deploy-s3: + description: Whether to deploy images to S3 + required: false + type: boolean + default: true + deploy-hostapp: + description: Whether to deploy a hostApp container image to a balena environment + required: false + type: boolean + default: true + deploy-ami: + description: Whether to deploy an AMI to AWS + required: false + type: boolean + default: false + sign-image: + description: Whether to sign image for secure boot + required: false + type: boolean + default: false + os-dev: + description: Enable OS development features + required: false + type: boolean + default: false + deploy-esr: + description: Enable to deploy ESR + required: false + type: boolean + default: false + +# env: +# # balena environment URL (e.g. balena-cloud.com) +# balena-url: ${{ vars.BALENA_URL || 'balena-cloud.com' }} +# # balenaCloud Organization for deploying hostapp releases +# hostapp-org: ${{ vars.HOSTAPP_ORG || 'balena_os' }} +# # S3 region (e.g. 'us-east-1') for deploying images to images and AMIs to S3 +# s3-region: ${{ vars.S3_REGION || 'us-east-1' }} +# # S3 bucket (e.g. resin-production-img-cloudformation) for deploying images to images and AMIs to S3 +# s3-bucket: ${{ vars.S3_BUCKET || 'resin-production-img-cloudformation' }} +# # AWS subnet for AMI deploys +# aws-subnet: ${{ vars.AWS_SUBNET || 'subnet-02d18a08ea4058574' }} +# # AWS security group for AMI deploys +# aws-security-group: ${{ vars.AWS_SECURITY_GROUP || 'sg-057937f4d89d9d51c' }} +# # URL for secure boot signing server API +# sign-api-url: ${{ vars.SIGN_API_URL || 'https://sign.balena-cloud.com' }} +# # Yocto NFS sstate cache host +# yocto-cache-host: ${{ vars.YOCTO_CACHE_HOST || 'nfs.product-os.io' }} + +# https://docs.github.com/en/actions/using-jobs/using-concurrency +concurrency: + group: ${{ github.workflow }}-${{ github.event.number || github.ref }} + # cancel jobs in progress for updated PRs, but not merge or tag events + cancel-in-progress: ${{ github.event.action == 'synchronize' }} + +jobs: + build: + name: Build + runs-on: ${{ fromJSON(inputs.runs-on) }} + environment: ${{ inputs.environment }} + + env: + MACHINE: "${{ inputs.machine }}" + automation_dir: "${{ github.workspace }}/balena-yocto-scripts/automation" + BALENARC_BALENA_URL: "${{ inputs.balena-url }}" + WORKSPACE: ${{ github.workspace }} + VERBOSE: verbose + YOCTO_CACHE_DIR: ${{ github.workspace }}/shared/yocto-cache + BARYS_ARGUMENTS_VAR: "" + + outputs: + hostapp_path: ${{ steps.build.outputs.hostapp_path }} + os_version: ${{ steps.build-lib.outputs.os_version }} + device_slug: ${{ steps.build-lib.outputs.device_slug }} + deploy_artifact: ${{ steps.build-lib.outputs.deploy_artifact }} + is_private: ${{ steps.build-lib.outputs.is_private }} + dt_arch: ${{ steps.build-lib.outputs.dt_arch }} + meta_balena_version: ${{ steps.build-lib.outputs.meta_balena_version }} + yocto_scripts_ref: ${{ steps.build-lib.outputs.yocto_scripts_ref }} + + defaults: + run: + working-directory: . + shell: bash --noprofile --norc -eo pipefail -x {0} + + steps: + # https://github.com/product-os/flowzone/blob/d92a0f707ca791ea4432306fcb35008848cc9bcb/flowzone.yml#L449-L473 + - name: Reject unapproved external contributions + env: + ok_to_test_label: ok-to-test + # https://cli.github.com/manual/gh_help_environment + GH_DEBUG: "true" + GH_PAGER: "cat" + GH_PROMPT_DISABLED: "true" + GH_REPO: "${{ inputs.device-repo }}" + GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + if: | + github.event.pull_request.state == 'open' && + github.event.pull_request.head.repo.full_name != github.repository + run: | + pr_labels="$(gh pr view ${{ github.event.pull_request.number }} --json labels -q .labels[].name)" + + for label in "${pr_labels}" + do + if [[ "$label" =~ "${{ env.ok_to_test_label }}" ]] + then + gh pr edit ${{ github.event.pull_request.number }} --remove-label "${{ env.ok_to_test_label }}" + exit 0 + fi + done + + echo "::error::External contributions must be approved with the label '${{ env.ok_to_test_label }}'. \ + Please contact a member of the organization for assistance." + exit 1 + + # this must be done before putting files in the workspace + # https://github.com/easimon/maximize-build-space + - name: Maximize build space + if: inputs.yocto-cache-host != '' && contains(fromJSON(inputs.runs-on), 'self-hosted') == false + uses: easimon/maximize-build-space@fc881a613ad2a34aca9c9624518214ebc21dfc0c + with: + root-reserve-mb: '4096' + temp-reserve-mb: '1024' + swap-size-mb: '4096' + remove-dotnet: 'true' + remove-android: 'true' + remove-haskell: 'true' + remove-codeql: 'true' + remove-docker-images: 'true' + + # https://github.com/actions/checkout + - name: Clone device repository + uses: actions/checkout@v3 + with: + repository: ${{ inputs.device-repo }} + token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ inputs.device-repo-ref }} + submodules: true + + - name: Device repository check + run: | + if [ "$(yq '.type' repo.yml)" != "yocto-based OS image" ]; then + echo "::error::Repository does not appear to be of type 'yocto-based OS image'" + exit 1 + fi + + - name: Update meta-balena submodule to ${{ inputs.meta-balena-ref }} + if: inputs.meta-balena-ref != '' + working-directory: ./layers/meta-balena + run: | + git config --add remote.origin.fetch '+refs/pull/*:refs/remotes/origin/pr/*' + git fetch --all + git checkout --force "${{ inputs.meta-balena-ref }}" + git submodule update --init --recursive + + - name: Update balena-yocto-scripts submodule to ${{ inputs.yocto-scripts-ref }} + if: inputs.yocto-scripts-ref != '' + working-directory: ./balena-yocto-scripts + run: | + git config --add remote.origin.fetch '+refs/pull/*:refs/remotes/origin/pr/*' + git fetch --all + git checkout --force "${{ inputs.yocto-scripts-ref }}" + git submodule update --init --recursive + + - name: Set build outputs + id: build-lib + run: | + source "${automation_dir}/include/balena-api.inc" + source "${automation_dir}/include/balena-lib.inc" + + ./balena-yocto-scripts/build/build-device-type-json.sh + + device_slug="$(balena_lib_get_slug "${MACHINE}")" + echo "device_slug=${device_slug}" >> $GITHUB_OUTPUT + + os_version="$(balena_lib_get_os_version)" + echo "os_version=${os_version}" >> $GITHUB_OUTPUT + + meta_balena_version="$(balena_lib_get_meta_balena_base_version)" + echo "meta_balena_version=${meta_balena_version}" >> $GITHUB_OUTPUT + + yocto_scripts_ref="$(git submodule status balena-yocto-scripts | awk '{print $1}')" + echo "yocto_scripts_ref=${yocto_scripts_ref}" >> $GITHUB_OUTPUT + + deploy_artifact="$(balena_lib_get_deploy_artifact "${MACHINE}")" + echo "deploy_artifact=${deploy_artifact}" >> $GITHUB_OUTPUT + + dt_arch="$(balena_lib_get_dt_arch "${MACHINE}")" + echo "dt_arch=${dt_arch}" >> $GITHUB_OUTPUT + + is_private="$(balena_api_is_dt_private "${{ inputs.machine }}")" + echo "is_private=${is_private}" >> $GITHUB_OUTPUT + + if [ ! -f "${WORKSPACE}/balena.yml" ]; then + _contract=$(balena_lib_build_contract "${device_slug}") + cp "${_contract}" "${WORKSPACE}/balena.yml" + fi + + sudo mkdir -p "${YOCTO_CACHE_DIR}" + sudo chown $(id -u):$(id -g) "${YOCTO_CACHE_DIR}" + + - name: Enable OS development features + if: inputs.os-dev == true + run: | + if [ "${OS_DEVELOPMENT}" = "true" ]; then + echo BARYS_ARGUMENTS_VAR="${BARYS_ARGUMENTS_VAR} -d" >> $GITHUB_ENV + fi + + - name: Enable signed images + if: inputs.sign-image == true && inputs.sign-api-url != '' + env: + SIGN_API: "${{ inputs.sign-api-url }}" + SIGN_API_KEY: "${{ secrets.SIGN_API_KEY }}" + SIGN_GRUB_KEY_ID: 2EB29B4CE0132F6337897F5FB8A88D1C62FCC729 + SIGN_KMOD_KEY_APPEND: "${{ secrets.SIGN_KMOD_KEY_APPEND }}" + run: | + echo "BARYS_ARGUMENTS_VAR=${BARYS_ARGUMENTS_VAR} -a SIGN_API=${SIGN_API} -a SIGN_API_KEY=${SIGN_API_KEY} -a SIGN_GRUB_KEY_ID=${SIGN_GRUB_KEY_ID} -a SIGN_KMOD_KEY_APPEND=${SIGN_KMOD_KEY_APPEND} --bb-args --no-setscene" >> $GITHUB_ENV + + - name: Mount shared cache + if: inputs.yocto-cache-host != '' && contains(fromJSON(inputs.runs-on), 'self-hosted') + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends nfs-common + sudo mount -t nfs "${{ inputs.yocto-cache-host }}:/" "${YOCTO_CACHE_DIR}" -o fsc + + - name: Build + id: build + run: | + ./balena-yocto-scripts/build/balena-build.sh -d "${MACHINE}" -t "${{ secrets.BALENA_API_KEY }}" -s "${YOCTO_CACHE_DIR}" -g "${BARYS_ARGUMENTS_VAR}" + hostapp_path=$(find "${WORKSPACE}/build/tmp/deploy/" -name "balena-image-${MACHINE}.docker" -type l || true) + if [ -n "${hostapp_path}" ]; then + hostapp_path="$(readlink --canonicalize "${hostapp_path}")"" + echo "hostapp_path=${hostapp_path}" >> $GITHUB_OUTPUT + else + echo "::error::No hostapp found in build artifacts" + exit 1 + fi + + - name: Prepare artifacts + id: prepare + run: | + source "${automation_dir}/include/balena-deploy.inc" + balena_deploy_artifacts "${{ inputs.machine }}" "${WORKSPACE}/gh-deploy" false + tar --auto-compress -cvf "${{ runner.temp }}/build-artifacts.tar.zst" "${WORKSPACE}/gh-deploy" + + # https://github.com/actions/upload-artifact + - name: Upload artifacts + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 + with: + name: build-artifacts + path: ${{ runner.temp }}/build-artifacts.tar.zst + retention-days: 3 + + s3-deploy: + name: S3 Deploy + runs-on: ${{ fromJSON(inputs.runs-on) }} + environment: ${{ inputs.environment }} + needs: build + if: inputs.deploy-s3 == true + + env: + MACHINE: "${{ inputs.machine }}" + BALENARC_BALENA_URL: "${{ inputs.balena-url }}" + WORKSPACE: ${{ github.workspace }} + VERBOSE: verbose + + defaults: + run: + working-directory: . + shell: bash --noprofile --norc -eo pipefail -x {0} + + steps: + # TODO: clone balena-yocto-scripts @ yocto_scripts_ref instead + # https://github.com/actions/checkout + - name: Clone device repository + uses: actions/checkout@v3 + with: + repository: ${{ inputs.device-repo || github.repository }} + token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ inputs.device-repo-ref || github.ref }} + submodules: true + + - name: Update balena-yocto-scripts submodule to ${{ inputs.yocto-scripts-ref }} + if: inputs.yocto-scripts-ref != '' + working-directory: ./balena-yocto-scripts + run: | + git config --add remote.origin.fetch '+refs/pull/*:refs/remotes/origin/pr/*' + git fetch --all + git checkout --force "${{ inputs.yocto-scripts-ref }}" + git submodule update --init --recursive + + # https://github.com/actions/download-artifact + - name: Download build artifacts + uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2 + with: + path: ${{ runner.temp }} + name: build-artifacts + + - name: Extract build artifacts + run: | + tar -xvf ${{ runner.temp }}/build-artifacts.tar.zst + find . + + - name: Set s3 outputs + id: s3-lib + run: | + echo "s3_policy=private" >> $GITHUB_OUTPUT + if [ "${{ needs.build.outputs.is_private }}" = "false" ]; then + echo "s3_policy=public-read" >> $GITHUB_OUTPUT + fi + + echo "deployer_uid=$(id -u)" >> $GITHUB_OUTPUT + echo "deployer_gid=$(id -g)" >> $GITHUB_OUTPUT + + s3_bucket_path="${{ inputs.s3-bucket}}/images" + if [ "${{ inputs.deploy-esr}}" = "true" ]; then + s3_bucket_path="${{ inputs.s3-bucket}}/esr-images" + fi + + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # login required to pull private balena/balena-img image + # https://github.com/docker/login-action + - name: Login to Docker Hub + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + with: + registry: docker.io + username: ${{ secrets.DOCKERHUB_USER }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + # TODO: use a prebuilt image for this if possible + # https://github.com/docker/build-push-action + - name: Build helper image + id: helper-image + uses: docker/build-push-action@v5.1.0 + with: + context: "${{ github.workspace }}/balena-yocto-scripts/automation" + file: Dockerfile_s3-deploy-env + push: false + build-args: | + DEPLOYER_UID=${{ steps.s3-lib.outputs.deployer_uid }} + DEPLOYER_GID=${{ steps.s3-lib.outputs.deployer_gid }} + + - name: Deploy to S3 + id: s3-deploy + env: + BASE_DIR: /host/images + S3_CMD: "s4cmd --access-key=${{ secrets.S3_ACCESS_KEY }} --secret-key=${{ secrets.S3_SECRET_KEY }} --API-ServerSideEncryption=AES256" + S3_SYNC_OPTS: "--recursive --API-ACL=${{ steps.s3-lib.outputs.s3_policy }}" + S3_BUCKET: "${{ inputs.s3-bucket }}" + SLUG: "${{ needs.build.outputs.device_slug }}" + DEPLOY_ARTIFACT: "${{ needs.build.outputs.deploy_artifact }}" + HOSTOS_VERSION: "${{ needs.build.outputs.os_version }}" + # TODO: images path must be found in artifacts + IMAGES_PATH: ${{ github.workspace }}/build-artifacts + run: | + docker run --rm -t --user deployer \ + -e BASE_DIR \ + -e S3_CMD \ + -e S3_SYNC_OPTS \ + -e S3_BUCKET \ + -e SLUG \ + -e DEPLOY_ARTIFACT \ + -e HOSTOS_VERSION \ + -v "${IMAGES_PATH}:/host/images" \ + "${{ steps.helper-image.outputs.imageid }}" /bin/sh -e -c '\ + echo "${HOSTOS_VERSION}" > "/host/images/${SLUG}/latest" + if [ "$DEPLOY_ARTIFACT" != "docker-image" ]; then + /usr/src/app/node_modules/.bin/ts-node /usr/src/app/scripts/prepare.ts + fi + if [ -z "$($S3_CMD ls s3://${S3_BUCKET}/${SLUG}/${HOSTOS_VERSION}/)" ] || [ -n "$($S3_CMD ls s3://${S3_BUCKET}/${SLUG}/${HOSTOS_VERSION}/IGNORE)" ]; then + touch /host/images/${SLUG}/${HOSTOS_VERSION}/IGNORE + $S3_CMD del -rf s3://${S3_BUCKET}/${SLUG}/${HOSTOS_VERSION} + $S3_CMD put /host/images/${SLUG}/${HOSTOS_VERSION}/IGNORE s3://${S3_BUCKET}/${SLUG}/${HOSTOS_VERSION}/ + $S3_CMD $S3_SYNC_OPTS dsync /host/images/${SLUG}/${HOSTOS_VERSION}/ s3://${S3_BUCKET}/${SLUG}/${HOSTOS_VERSION}/ + $S3_CMD put /host/images/${SLUG}/latest s3://${S3_BUCKET}/${SLUG}/ --API-ACL=public-read -f + $S3_CMD del s3://${S3_BUCKET}/${SLUG}/${HOSTOS_VERSION}/IGNORE + else + echo "WARNING: Deployment already done for ${SLUG} at version ${HOSTOS_VERSION}" + fi + ' + + ami-deploy: + name: AMI Deploy + runs-on: ${{ fromJSON(inputs.runs-on) }} + environment: ${{ inputs.environment }} + needs: build + if: inputs.deploy-ami == true + + env: + MACHINE: "${{ inputs.machine }}" + BALENARC_BALENA_URL: "${{ inputs.balena-url }}" + WORKSPACE: ${{ github.workspace }} + VERBOSE: verbose + + defaults: + run: + working-directory: . + shell: bash --noprofile --norc -eo pipefail -x {0} + + steps: + # TODO: clone balena-yocto-scripts @ yocto_scripts_ref instead + # https://github.com/actions/checkout + - name: Clone device repository + uses: actions/checkout@v3 + with: + repository: ${{ inputs.device-repo || github.repository }} + token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ inputs.device-repo-ref || github.ref }} + submodules: true + + - name: Update balena-yocto-scripts submodule to ${{ inputs.yocto-scripts-ref }} + if: inputs.yocto-scripts-ref != '' + working-directory: ./balena-yocto-scripts + run: | + git config --add remote.origin.fetch '+refs/pull/*:refs/remotes/origin/pr/*' + git fetch --all + git checkout --force "${{ inputs.yocto-scripts-ref }}" + git submodule update --init --recursive + + # https://github.com/actions/download-artifact + - name: Download build artifacts + uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2 + with: + path: ${{ runner.temp }} + name: build-artifacts + + - name: Extract build artifacts + run: | + tar -xvf ${{ runner.temp }}/build-artifacts.tar.zst + find . + + - name: Set AMI outputs + id: ami-lib + run: | + if [ "${dt_arch}" = "amd64" ]; then + echo "ami_arch=x86_64" >> $GITHUB_OUTPUT + elif [ "${dt_arch}" = "aarch64" ]; then + echo "ami_arch=arm64" >> $GITHUB_OUTPUT + fi + + # AMI name format: balenaOS(-installer?)(-secureboot?)-VERSION-DEVICE_TYPE + if [ "${{ inputs.sign-image }}" = "true" ]; then + echo "ami_name=balenaOS-secureboot-${VERSION}-${MACHINE}" >> $GITHUB_OUTPUT + else + echo "ami_name=balenaOS-${VERSION}-${MACHINE}" >> $GITHUB_OUTPUT + fi + + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # TODO: use a prebuilt image for this + # https://github.com/docker/build-push-action + - name: Build helper image + id: helper-image + uses: docker/build-push-action@v5.1.0 + with: + context: "${{ github.workspace }}/balena-yocto-scripts/automation" + file: Dockerfile_yocto-build-env + target: yocto-generate-ami-env + push: false + + - name: Deploy AMI + env: + S3_REGION: "${{ inputs.s3-region }}" + S3_BUCKET: "${{ inputs.s3-bucket }}" + AWS_ACCESS_KEY_ID: "${{ secrets.S3_ACCESS_KEY}}" + AWS_SECRET_ACCESS_KEY: "${{ secrets.S3_SECRET_KEY }}" + AWS_DEFAULT_REGION: "${{ inputs.s3-region }}" + AWS_SESSION_TOKEN: "" # only required if MFA is enabled + AWS_SUBNET_ID: "${{ inputs.aws-subnet }}" + AWS_SECURITY_GROUP_ID: "${{ inputs.aws-security-group }}" + BALENACLI_TOKEN: ${{ secrets.BALENA_API_KEY }} + HOSTOS_VERSION: "${{ needs.build.outputs.os_version }}" + AMI_NAME: "${{ steps.ami-lib.outputs.ami_name }}" + AMI_ARCHITECTURE: "${{ steps.ami-lib.outputs.ami_arch }}" + AMI_SECUREBOOT: "${{ inputs.sign-image }}" + BALENA_PRELOAD_APP: "balena_os/cloud-config-${{ steps.ami-lib.outputs.ami_arch }}" + BALENA_PRELOAD_COMMIT: current + # TODO: image path must be found in artifacts + IMAGE: balena-image-${MACHINE}.balenaos-img + run: | + docker run --rm -t \ + --privileged \ + --network host \ + -v "${WORKSPACE}:${WORKSPACE}" \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -e VERBOSE \ + -e AWS_ACCESS_KEY_ID \ + -e AWS_SECRET_ACCESS_KEY \ + -e AWS_DEFAULT_REGION \ + -e AWS_SESSION_TOKEN \ + -e AMI_NAME \ + -e AMI_ARCHITECTURE \ + -e AMI_SECUREBOOT \ + -e S3_BUCKET \ + -e BALENA_PRELOAD_APP \ + -e BALENARC_BALENA_URL \ + -e BALENACLI_TOKEN \ + -e BALENA_PRELOAD_COMMIT \ + -e IMAGE \ + -e MACHINE \ + -e HOSTOS_VERSION \ + -e AWS_SUBNET_ID \ + -e AWS_SECURITY_GROUP_ID \ + -w "${WORKSPACE}" \ + "${{ steps.helper-image.outputs.imageid }}" /balena-generate-ami.sh + + balena-deploy: + name: HostApp Deploy + runs-on: ${{ fromJSON(inputs.runs-on) }} + environment: ${{ inputs.environment }} + needs: build + if: inputs.deploy-hostapp == true + + env: + MACHINE: "${{ inputs.machine }}" + BALENARC_BALENA_URL: "${{ inputs.balena-url }}" + WORKSPACE: ${{ github.workspace }} + VERBOSE: verbose + SLUG: "${{ needs.build.outputs.device_slug }}" + SECURE_BOOT: "${{ inputs.sign-image }}" + SIGN_API_URL: "${{ inputs.sign-api-url }}" + + defaults: + run: + working-directory: . + shell: bash --noprofile --norc -eo pipefail -x {0} + + steps: + # TODO: clone balena-yocto-scripts @ yocto_scripts_ref instead + # https://github.com/actions/checkout + - name: Clone device repository + uses: actions/checkout@v3 + with: + repository: ${{ inputs.device-repo || github.repository }} + token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ inputs.device-repo-ref || github.ref }} + submodules: true + + # https://github.com/actions/download-artifact + - name: Download build artifacts + uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2 + with: + path: ${{ runner.temp }} + name: build-artifacts + + - name: Extract build artifacts + run: | + tar -xvf ${{ runner.temp }}/build-artifacts.tar.zst + find . + + - name: Set deploy outputs + run: | + if [ -n "${{ inputs.sign-image }}" = "true" ]; then + echo "SECURE_BOOT_FEATURE_FLAG=yes" >> $GITHUB_ENV + else + echo "SECURE_BOOT_FEATURE_FLAG=no" >> $GITHUB_ENV + fi + + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # TODO: replace this with balena-io/deploy-to-balena-action when it supports deploy-only + # https://github.com/docker/build-push-action + - name: Build helper image + id: helper-image + uses: docker/build-push-action@v5.1.0 + with: + context: "${{ github.workspace }}/balena-yocto-scripts/automation" + file: Dockerfile_yocto-build-env + target: yocto-build-env + push: false + + # TODO: replace this with balena-io/deploy-to-balena-action when it supports deploy-only + # https://github.com/balena-io/deploy-to-balena-action/issues/286 + - name: Deploy to balena + env: + APPNAME: "${{ needs.build.outputs.device_slug }}" + API_ENV: "${{ inputs.balena-url }}" + BALENAOS_TOKEN: "${{ secrets.BALENA_API_KEY }}" + BALENAOS_ACCOUNT: "${{ inputs.hostapp-org }}" + META_BALENA_VERSION: "${{ needs.build.outputs.meta_balena_version }}" + RELEASE_VERSION: "${{ needs.build.outputs.os_version }}" + BOOTABLE: 1 + DEPLOY: yes + FINAL: yes + ESR: "${{ inputs.deploy-esr }}" + balenaCloudEmail: + balenaCloudPassword: + # TODO: hostapp path must be found in artifacts + HOSTAPP_PATH: "${{ needs.build.outputs.hostapp_path }}" + run: | + docker run --rm -t \ + --privileged \ + -e APPNAME \ + -e API_ENV \ + -e BALENAOS_TOKEN \ + -e BALENAOS_ACCOUNT \ + -e META_BALENA_VERSION \ + -e RELEASE_VERSION \ + -e MACHINE \ + -e VERBOSE \ + -e BOOTABLE \ + -e DEPLOY \ + -e FINAL \ + -e ESR \ + -e SECURE_BOOT_FEATURE_FLAG \ + -e balenaCloudEmail \ + -e balenaCloudPassword \ + -v "$(readlink --canonicalize "${HOSTAPP_PATH}")":/host/appimage.docker \ + -v "${WORKSPACE}":/work \ + -v "${WORKSPACE}":/deploy \ + "${{ steps.helper-image.outputs.imageid }}" /balena-deploy-block.sh diff --git a/automation/Dockerfile_s3-deploy-env b/automation/Dockerfile_s3-deploy-env new file mode 100644 index 000000000..91b98406c --- /dev/null +++ b/automation/Dockerfile_s3-deploy-env @@ -0,0 +1,13 @@ +# private base image requires docker login +FROM balena/balena-img:6.20.26 + +ARG DEPLOYER_UID +ARG DEPLOYER_GID + +# hadolint ignore=DL3008 +RUN apt-get update \ + && apt-get install -y --no-install-recommends s4cmd \ + && rm -rf /var/lib/apt/lists/* + +RUN groupadd -g $DEPLOYER_GID deployer \ + useradd -m -u $DEPLOYER_UID -g $DEPLOYER_GID deployer