From 5f13baf2d85158838d67a8de6a1fc92d0119a481 Mon Sep 17 00:00:00 2001 From: Devin Baca Date: Mon, 27 Jul 2026 14:49:34 -0700 Subject: [PATCH 1/2] Migrate CI from CircleCI to GitHub Actions Replaces .circleci/config.yml with two GHA workflows following the pattern established by smartrent_castore, alloy_access_atlas, and credo_binary_patterns for SmartRent Hex libraries: matrix build/test across two Elixir/OTP pairs, quality checks and hex.build on the latest version only, and a separate release workflow that verifies version alignment, cuts the GitHub release, and publishes to Hex on tag push. HEX_API_WRITE_KEY secret still needs to be added before the next tagged release can publish to Hex. --- .circleci/config.yml | 187 --------------------------------- .github/workflows/ci.yaml | 106 +++++++++++++++++++ .github/workflows/release.yaml | 55 ++++++++++ 3 files changed, 161 insertions(+), 187 deletions(-) delete mode 100644 .circleci/config.yml create mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/release.yaml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 11f040c..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,187 +0,0 @@ -version: 2.1 - -elixir_current: &elixir_current "1.19.2-erlang-28.1.1-ubuntu-noble-20251001" -elixir_prev: &elixir_prev "1.18.4-erlang-27.3.4.4-ubuntu-noble-20251001" - -defaults: &defaults - working_directory: ~/repo - environment: - LC_ALL: C.UTF-8 - MIX_ENV: test - parameters: - image-tag: - description: "Docker image tag" - type: string - default: *elixir_current - -commands: - install_system_deps: - steps: - - run: | - (type -p wget >/dev/null || (apt update && apt install wget -y)) \ - && mkdir -p -m 755 /etc/apt/keyrings \ - && out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \ - && cat $out | tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \ - && chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \ - && mkdir -p -m 755 /etc/apt/sources.list.d \ - && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ - && apt update \ - && apt install -y gh lrzsz ca-certificates \ - && mix do local.hex --force, local.rebar --force - restore_tagged_cache: - parameters: - prefix: - description: "Cache key prefix" - type: string - steps: - - restore_cache: - keys: - - << parameters.prefix >>-{{ .Branch }}-{{ checksum "mix.lock" }} - - << parameters.prefix >>-{{ .Branch }}- - - << parameters.prefix >>- - -jobs: - build: - <<: *defaults - docker: - - image: hexpm/elixir:<< parameters.image-tag >> - steps: - - checkout - - install_system_deps - - restore_tagged_cache: - prefix: v1-mix-cache-<< parameters.image-tag >> - - run: mix do deps.get, compile --warnings-as-errors - - run: MIX_ENV=docs mix do deps.get, compile --warnings-as-errors - - save_cache: - key: v1-mix-cache-<< parameters.image-tag >>-{{ .Branch }}-{{ checksum "mix.lock" }} - paths: - - _build - - deps - - lint: - <<: *defaults - docker: - - image: hexpm/elixir:<< parameters.image-tag >> - steps: - - checkout - - install_system_deps - - restore_tagged_cache: - prefix: v1-mix-cache-<< parameters.image-tag >> - - run: mix deps.unlock --check-unused - - run: mix format --check-formatted - - run: mix credo -a - - run: mix hex.build - - run: MIX_ENV=docs mix docs --warnings-as-errors - - dialyzer: - <<: *defaults - docker: - - image: hexpm/elixir:<< parameters.image-tag >> - steps: - - checkout - - install_system_deps - - restore_tagged_cache: - prefix: v1-mix-cache-<< parameters.image-tag >> - - restore_tagged_cache: - prefix: v1-dialyzer-cache-<< parameters.image-tag >> - - run: mix dialyzer - - save_cache: - paths: - - _build/plts - key: v1-dialyzer-cache-<< parameters.image-tag >>-{{ .Branch }}-{{ checksum "mix.lock" }} - - test: - <<: *defaults - docker: - - image: hexpm/elixir:<< parameters.image-tag >> - steps: - - checkout - - install_system_deps - - restore_tagged_cache: - prefix: v1-mix-cache-<< parameters.image-tag >> - - run: - name: Run tests - command: | - circleci tests glob 'test/**/*_test.exs' | \ - circleci tests run --split-by=timings --command='xargs -n1 echo > test_file_paths.txt' - cat test_file_paths.txt | xargs mix test - - store_test_results: - path: test-junit-report.xml - - publish: - <<: *defaults - docker: - - image: hexpm/elixir:<< parameters.image-tag >> - steps: - - checkout - - install_system_deps - - restore_tagged_cache: - prefix: v1-mix-cache-<< parameters.image-tag >> - - run: - name: Create GitHub Release - command: | - awk -v ver="<< pipeline.git.tag >>" '/^#+ \[/ { if (p) { exit }; if ($2 == "["ver"]") { p=1; next} } p && NF' CHANGELOG.md > release-notes.md - gh release create << pipeline.git.tag >> --title << pipeline.git.tag >> -F release-notes.md - - run: - name: Publish Hex - command: MIX_ENV=docs HEX_API_KEY=${HEX_API_WRITE_KEY} mix hex.publish --yes - - spelling_and_markdownlint: - working_directory: ~/repo - environment: - LC_ALL: C.UTF-8 - docker: - - image: node:24-alpine - steps: - - checkout - - run: - name: Setup - command: | - npm i -g cspell markdownlint-cli2 - - run: - name: Spell check - command: cspell --quiet . - - run: - name: Lint markdown files - command: markdownlint-cli2 - -workflows: - build_test: - # max_auto_reruns: 3 - jobs: - - build: - filters: pipeline.git.tag - matrix: - parameters: - image-tag: - - *elixir_current - - *elixir_prev - - lint: - filters: pipeline.git.tag - image-tag: *elixir_current - requires: - - build - - dialyzer: - filters: pipeline.git.tag - image-tag: *elixir_current - requires: - - build - - test: - filters: pipeline.git.tag - requires: - - build - matrix: - parameters: - image-tag: - - *elixir_current - - *elixir_prev - - publish: - context: - - github-token - filters: pipeline.git.tag starts-with "v" - image-tag: *elixir_current - requires: - - lint - - dialyzer - - test - - spelling_and_markdownlint diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..b7e84cb --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,106 @@ +name: Elixir CI + +on: + push: + branches: + - main + - hotfix/[0-9]+.[0-9]+.[0-9]+ + - release/[0-9]+.[0-9]+.[0-9]+ + tags: + - "*" + pull_request: + types: [synchronize, opened, reopened] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: test (${{ matrix.elixir }}-erlang-${{ matrix.erlang }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - elixir: "1.19.2" + erlang: "28.1.1" + latest: true + - elixir: "1.18.4" + erlang: "27.3.4.4" + latest: false + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1 + with: + elixir-version: ${{ matrix.elixir }} + otp-version: ${{ matrix.erlang }} + + - name: Install lrzsz + run: sudo apt-get update && sudo apt-get install -y lrzsz + + - name: Restore deps/build cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + deps + _build + key: ${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.erlang }}-${{ hashFiles('mix.lock') }} + restore-keys: | + ${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.erlang }}- + + - name: Restore Dialyzer PLT cache (latest only) + if: matrix.latest + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: _build/plts + key: ${{ runner.os }}-plt-${{ matrix.elixir }}-${{ matrix.erlang }}-${{ hashFiles('mix.lock') }} + restore-keys: | + ${{ runner.os }}-plt-${{ matrix.elixir }}-${{ matrix.erlang }}- + + - run: mix deps.get + + - run: mix compile --warnings-as-errors + + - run: mix test + + - name: Quality checks (latest only) + if: matrix.latest + run: | + mix deps.unlock --check-unused + mix format --check-formatted + mix credo -a + mix dialyzer + env: + MIX_ENV: test + + - name: Docs + hex build (latest only) + if: matrix.latest + run: | + mix deps.get + mix docs --warnings-as-errors + mix hex.build + env: + MIX_ENV: docs + + spelling_and_markdownlint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + + - name: Install cspell and markdownlint-cli2 + run: npm i -g cspell markdownlint-cli2 + + - name: Spell check + run: cspell --quiet . + + - name: Lint markdown files + run: markdownlint-cli2 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..fbefdd9 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,55 @@ +name: Release + +on: + push: + tags: + - "v*" + +permissions: + contents: write # required for `gh release create` via the built-in GITHUB_TOKEN + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Verify matching version in CHANGELOG.md, tag, and mix.exs + run: | + VERSION=$(sed -n 's/.*@version "\([^"]*\)".*/\1/p' mix.exs) + if ! grep -q "## \[v$VERSION\]" CHANGELOG.md; then + echo "Error: Version v$VERSION not found in CHANGELOG.md" + exit 1 + fi + TAG_VERSION=${GITHUB_REF_NAME#v} + if [ "$TAG_VERSION" != "$VERSION" ]; then + echo "Error: Tag version ($TAG_VERSION) does not match mix.exs version ($VERSION)" + exit 1 + fi + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + awk -v ver="${GITHUB_REF_NAME}" '/^#+ \[/ { if (p) { exit }; if ($2 == "["ver"]") { p=1; next} } p && NF' CHANGELOG.md > release-notes.md + gh release create "${GITHUB_REF_NAME}" \ + --title "${GITHUB_REF_NAME}" \ + -F release-notes.md \ + --repo "${GITHUB_REPOSITORY}" + + - uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1 + with: + elixir-version: "1.19.2" + otp-version: "28.1.1" + + - name: Publish to Hex + env: + MIX_ENV: docs + HEX_API_KEY: ${{ secrets.HEX_API_WRITE_KEY }} + run: | + mix deps.get + mix hex.publish --yes From e258d3f0c4e005c6c4e8aebbf44a9f940b761fb4 Mon Sep 17 00:00:00 2001 From: Devin Baca Date: Mon, 27 Jul 2026 15:02:09 -0700 Subject: [PATCH 2/2] Set MIX_ENV=test at the job level in ci.yaml The compile/test steps ran under Mix's default :dev env instead of :test, unlike the CircleCI config which set MIX_ENV=test globally. This meant --warnings-as-errors wasn't validating test-support code (elixirc_paths(:test) includes test/support), where CircleCI's identical compile step did catch it. --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b7e84cb..9093894 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -23,6 +23,8 @@ jobs: test: name: test (${{ matrix.elixir }}-erlang-${{ matrix.erlang }}) runs-on: ubuntu-latest + env: + MIX_ENV: test strategy: fail-fast: false matrix: @@ -77,8 +79,6 @@ jobs: mix format --check-formatted mix credo -a mix dialyzer - env: - MIX_ENV: test - name: Docs + hex build (latest only) if: matrix.latest