Created
August 13, 2025 19:40
-
-
Save jhosteny/c265c75adf04c01ef567ee8b0f8480ad to your computer and use it in GitHub Desktop.
Phoenix GitHub Actions CI
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: Build and Test Workflow | |
| on: | |
| workflow_call: | |
| inputs: | |
| dialyzer: | |
| description: Whether to run the dialyzer | |
| type: boolean | |
| required: false | |
| default: false | |
| env: | |
| MIX_ENV: test | |
| jobs: | |
| extract_versions: | |
| name: Extract info from .tool-versions | |
| runs-on: ubuntu-latest | |
| outputs: | |
| elixir-version: ${{ steps.set-versions.outputs.elixir_version }} | |
| otp-version: ${{ steps.set-versions.outputs.otp_version }} | |
| steps: | |
| - name: Checkout .tool-versions file | |
| uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 | |
| with: | |
| sparse-checkout: | | |
| .tool-versions | |
| sparse-checkout-cone-mode: false | |
| - name: Set Elixir, OTP, and Node.js versions as output | |
| id: set-versions | |
| run: | | |
| elixir_version=$(grep -h elixir .tool-versions | awk '{ print $2 }' | awk -F - '{print $1}') | |
| otp_version=$(grep -h erlang .tool-versions | awk '{ print $2 }') | |
| nodejs_version=$(grep -h nodejs .tool-versions | awk '{ print $2 }') | |
| echo "elixir_version=$elixir_version" >> $GITHUB_OUTPUT | |
| echo "otp_version=$otp_version" >> $GITHUB_OUTPUT | |
| echo "nodejs_version=$nodejs_version" >> $GITHUB_OUTPUT | |
| test: | |
| name: Test on OTP ${{ needs.extract_versions.outputs.otp-version }} / Elixir ${{ needs.extract_versions.outputs.elixir-version }} | |
| runs-on: ubuntu-latest | |
| needs: extract_versions | |
| env: | |
| otp-version: ${{ needs.extract_versions.outputs.otp-version }} | |
| elixir-version: ${{ needs.extract_versions.outputs.elixir-version }} | |
| services: | |
| db: | |
| image: postgres:16.6 | |
| ports: ["5432:5432"] | |
| env: | |
| POSTGRES_PASSWORD: postgres | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| steps: | |
| - name: Set up Elixir | |
| uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.20.4 | |
| with: | |
| otp-version: ${{ env.otp-version }} | |
| elixir-version: ${{ env.elixir-version }} | |
| - name: Checkout code | |
| uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 | |
| with: | |
| repository: <org>/<repository> | |
| fetch-depth: 0 | |
| - name: Cache deps | |
| id: cache-deps | |
| uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 | |
| env: | |
| cache-name: cache-elixir-deps | |
| with: | |
| path: deps | |
| key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-mix-${{ env.cache-name }}- | |
| # Define how to cache the `_build` directory. After the first run, this | |
| # speeds up tests runs a lot. This includes not re-compiling our project's | |
| # downloaded deps every run. | |
| - name: Cache compiled build | |
| id: cache-build | |
| uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 | |
| env: | |
| cache-name: cache-compiled-build | |
| with: | |
| path: _build | |
| key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-mix-${{ env.cache-name }}- | |
| ${{ runner.os }}-mix- | |
| # Conditionally bust the cache when job is re-run. Sometimes, we may have | |
| # issues with incremental builds that are fixed by doing a full recompile. | |
| # In order to not waste dev time on such trivial issues (while also | |
| # reaping the time savings of incremental builds for *most* day-to-day | |
| # development), force a full recompile only on builds that are retried. | |
| - name: Clean to rule out incremental build as a source of flakiness | |
| if: github.run_attempt != '1' | |
| run: | | |
| mix deps.clean --all | |
| mix clean | |
| shell: sh | |
| - name: Install dependencies | |
| run: mix deps.get | |
| - name: Check for unused dependencies | |
| run: mix deps.unlock --check-unused | |
| - name: Check for abandoned packages | |
| run: mix hex.audit | |
| - name: Compiles without warnings | |
| run: mix compile --warnings-as-errors | |
| - name: Check Formatting | |
| run: mix format --check-formatted | |
| - name: Check code analysis | |
| run: mix credo --strict | |
| # mix gettext.extract --check-up-to-date will only check .pot files and | |
| # not the .po file associated. We check that manually here. | |
| - name: Compare gettext files | |
| run: | | |
| diff <(grep -v '^##' priv/gettext/en/LC_MESSAGES/errors.po | grep msgid) <(grep -v '^##' priv/gettext/errors.pot | grep msgid) | |
| diff <(grep -v '^##' priv/gettext/en/LC_MESSAGES/default.po | grep msgid) <(grep -v '^##' priv/gettext/default.pot | grep msgid) | |
| - name: Check if POT files are up to date | |
| run: mix gettext.extract --check-up-to-date | |
| - name: Check migrations | |
| run: mix excellent_migrations.check_safety | |
| - name: Test seeds | |
| run: mix ecto.drop --quiet && mix ecto.create --quiet && mix ecto.migrate --quiet && mix run priv/repo/seeds.exs | |
| - name: Run Sobelow security scan | |
| run: mix sobelow --config | |
| - name: Restore PLT cache | |
| id: plt_cache | |
| if: ${{ inputs.dialyzer }} | |
| uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 | |
| with: | |
| key: | | |
| plt-${{ runner.os }}-${{ env.otp-version }}-${{ env.elixir-version }}-${{ hashFiles('**/mix.lock') }} | |
| restore-keys: | | |
| plt-${{ runner.os }}-${{ env.otp-version }}-${{ env.elixir-version }}- | |
| path: | | |
| priv/plts | |
| - name: Create PLTs | |
| if: ${{ inputs.dialyzer && steps.plt_cache.outputs.cache-hit != 'true' }} | |
| run: MIX_ENV=dev mix dialyzer --plt | |
| # By default, the GitHub Cache action will only save the cache if all steps in the job succeed, | |
| # so we separate the cache restore and save steps in case running dialyzer fails. | |
| - name: Save PLT cache | |
| id: plt_cache_save | |
| uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 | |
| if: ${{ inputs.dialyzer && steps.plt_cache.outputs.cache-hit != 'true' }} | |
| with: | |
| key: | | |
| plt-${{ runner.os }}-${{ env.otp-version }}-${{ env.elixir-version }}-${{ hashFiles('**/mix.lock') }} | |
| path: | | |
| priv/plts | |
| - name: Run dialyzer | |
| if: ${{ inputs.dialyzer }} | |
| run: MIX_ENV=dev mix dialyzer --format github | |
| test_frontend: | |
| name: Test frontend | |
| runs-on: ubuntu-latest | |
| needs: extract_versions | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 | |
| with: | |
| repository: <org>/<repository> | |
| - name: Setup Node.js | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 | |
| with: | |
| node-version: ${{ needs.extract_versions.outputs.nodejs_version }} | |
| - name: Install npm dependencies | |
| working-directory: ./assets | |
| run: npm install | |
| - name: Check frontend formatting | |
| working-directory: ./assets | |
| run: npm run check-formatting |
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: Deploy Workflow | |
| on: | |
| workflow_call: | |
| inputs: | |
| role: | |
| description: CI role | |
| type: string | |
| required: true | |
| repository: | |
| description: ECS repository | |
| type: string | |
| required: true | |
| mix_env: | |
| description: Mix environment | |
| type: string | |
| required: true | |
| cluster: | |
| description: Cluster name | |
| type: string | |
| required: true | |
| service: | |
| description: Service name | |
| type: string | |
| required: true | |
| task: | |
| description: Task name | |
| type: string | |
| required: true | |
| container: | |
| description: Container name | |
| type: string | |
| required: true | |
| jobs: | |
| deploy: | |
| runs-on: ubuntu-latest | |
| environment: ${{ inputs.mix_env }} | |
| steps: | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 | |
| - name: Checkout code | |
| uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 | |
| - name: Report dependencies | |
| uses: erlef/mix-dependency-submission@bdccfd60e12db8f77147dc6024758e459025f5ee # v1.2.1 | |
| - name: Login to Docker Hub, to avoid rate limits | |
| uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USER }} | |
| password: ${{ secrets.DOCKERHUB_PASS }} | |
| - name: Configure AWS credentials | |
| uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1 | |
| with: | |
| role-to-assume: ${{ inputs.role }} | |
| role-session-name: ecr-ecs-deploy | |
| aws-region: <region> | |
| - name: Login to Amazon ECR | |
| id: login-ecr | |
| uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1 | |
| - name: Get short tag and repo URI | |
| id: info | |
| run: | | |
| IMAGE_TAG=$(echo ${{ github.sha }} | cut -c 1-7) | |
| echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT | |
| REPO_URI=${{ steps.login-ecr.outputs.registry }}/${{ inputs.repository }} | |
| echo "repo_uri=$REPO_URI" >> $GITHUB_OUTPUT | |
| echo "image=$REPO_URI:$IMAGE_TAG" >> $GITHUB_OUTPUT | |
| - name: Build, tag, and push builder image to Amazon ECR | |
| uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 | |
| with: | |
| push: true | |
| target: builder | |
| build-args: | |
| MIX_ENV=${{ inputs.mix_env }} | |
| cache-from: type=registry,ref=${{ steps.info.outputs.repo_uri }}:builder | |
| cache-to: type=registry,mode=max,image-manifest=true,oci-mediatypes=true,ref=${{ steps.info.outputs.repo_uri }}:builder | |
| tags: ${{ steps.info.outputs.repo_uri }}:builder | |
| - name: Build, tag, and push application image to Amazon ECR | |
| uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 | |
| with: | |
| push: true | |
| build-args: | |
| MIX_ENV=${{ inputs.mix_env }} | |
| cache-from: | | |
| type=registry,ref=${{ steps.info.outputs.repo_uri }}:builder | |
| type=registry,ref=${{ steps.info.outputs.repo_uri }}:latest | |
| cache-to: type=registry,mode=max,image-manifest=true,oci-mediatypes=true,ref=${{ steps.info.outputs.repo_uri }}:latest | |
| tags: | | |
| ${{ steps.info.outputs.repo_uri }}:${{ steps.info.outputs.image_tag }} | |
| ${{ steps.info.outputs.repo_uri }}:latest | |
| - name: Download existing ECS task definition | |
| id: task-def-download | |
| run: | | |
| aws ecs describe-task-definition --task-definition ${{ inputs.task }} --query taskDefinition | jq -r 'del( | |
| .taskDefinitionArn, | |
| .requiresAttributes, | |
| .compatibilities, | |
| .revision, | |
| .status, | |
| .registeredAt, | |
| .registeredBy | |
| )' > task-definition.json | |
| - name: Fill in the new image ID in the Amazon ECS task definition | |
| id: task-def | |
| uses: aws-actions/amazon-ecs-render-task-definition@64aefa8f68c9083d24d230e3099d046d5964bcba # v1.7.5 | |
| with: | |
| task-definition: task-definition.json | |
| container-name: ${{ inputs.container }} | |
| image: ${{ steps.info.outputs.image }} | |
| - name: Deploy Amazon ECS task definition | |
| uses: aws-actions/amazon-ecs-deploy-task-definition@4b08990e8909cf36bc2ca95f994312f090c41865 # v2.3.4 | |
| with: | |
| task-definition: ${{ steps.task-def.outputs.task-definition }} | |
| service: ${{ inputs.service }} | |
| cluster: ${{ inputs.cluster }} | |
| wait-for-service-stability: true |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment