When my apps were still on the now-deprecated platform version of Elastic Beanstalk (pre-Amazon Linux 2), my deployment process was simple. I’d create a release on GitHub, download the “Source code (zip)” file GitHub automatically creates, and upload it to the Elastic Beanstalk console.

When I started the migration to the new Amazon Linux 2 platform version, I quickly found that the GitHub zip file no longer worked, because it contains a root directory. Here’s what GitHub creates, and what works on the older version of Elastic Beanstalk:

myapp-1.0.2.zip
↳ myapp/
  ↳ .ebextensions/
    ↳ 01_setup.config

And here’s what the new Elastic Beanstalk requires:

myapp-1.0.2.zip
↳ .ebextensions/
  ↳ 01_setup.config

At first I wrote a simple Bash script that would move the contents of the zip file “up” one directory level. It worked, but I didn’t like the manual step between getting the release from GitHub and uploading to Elastic Beanstalk. I wanted to have confidence that my code hadn’t been tampered with after the release was created, even if I was the one doing the tampering.

I’d seen other projects using GitHub Actions to create release artifacts automatically, and it seemd a good fit, so I made one by piecing together code from the actions of a couple open source projects.

Here’s the package.yml I use now:

# AWS Elastic Beanstalk platform version 3 requires
# files in the deployment zip file to exist at the
# root of the zip file. The zip file GitHub creates
# for a release (named "Source code (zip)") has a
# directory at its root that contains all other
# files and directories in the repository, making
# it unsuitable for uploading to Elastic Beanstalk.
#
# This workflow builds a zip file that conforms to
# the structure that Elastic Beanstalk expects, and
# excludes all git and GitHub files.

name: Package release
on:
  release:
    types: [published]
permissions:
  contents: read
jobs:
  build:
    name: Build release package
    runs-on: ubuntu-latest
    environment: Build release package
    steps:
      - name: Checkout
        uses: actions/checkout@v2
        with:
          fetch-depth: 0
      - name: Build zip file
        run: |
          VERSION="$(echo "$GITHUB_REF" | cut -d/ -f3 | cut -dv -f2)"
          REPO_NAME="$(echo $GITHUB_REPOSITORY | cut -d/ -f2)"
          DIR="$(mktemp -d)"
          zip $DIR/release.zip -q -r --symlinks . -x *.git* *.pkg.zip
          mv $DIR/release.zip $REPO_NAME-$VERSION.pkg.zip
      - name: Upload artifact
        uses: actions/upload-artifact@v2
        with:
          name: package-release
          path: "*.pkg.zip"
  upload:
    name: Upload release package
    if: ${{ github.event_name == 'release' }}
    needs: build
    permissions:
      contents: write
    runs-on: ubuntu-latest
    steps:
      - name: Download artifact
        uses: actions/download-artifact@v2
        with:
          name: package-release
      - name: Upload release artifact
        uses: actions/github-script@v5
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const fs = require("fs").promises;
            const { repo: { owner, repo }, sha } = context;
            const tag = process.env.GITHUB_REF.replace("refs/tags/", "");
            const release = await github.rest.repos.getReleaseByTag({
              owner, repo, tag,
            });
            core.info(`Release: ${owner}/${repo}@${tag}`);
            for (let file of await fs.readdir(".")) {
              if (!file.endsWith(".pkg.zip")) continue;
              core.info(`Uploading: ${file}`);
              await github.rest.repos.uploadReleaseAsset({
                owner, repo,
                release_id: release.data.id,
                name: file,
                data: await fs.readFile(file),
              });
            }

This action checks out the repository at the release tag, creates a zip file containing the repository files (excluding .git*), and uploads it as an asset on the release.

The file name matches GitHub’s “Source code (zip)” download file name with .pkg added before .zip (e.g., GitHub’s file is downloaded as myapp-1.0.2.zip and this action creates myapp-1.0.2.pkg.zip).

Now I can create a release in GitHub, wait 30 seconds or so, then download the .pkg.zip file and upload it to Elastic Beanstalk to deploy.