name: ci
on:
  pull_request:
    paths-ignore:
      - "CHANGELOG.md"
      - "docs/**"
  push:
    branches:
      - main
  workflow_dispatch:

env:
  CARGO_TERM_COLOR: always
  RUSTFLAGS: "-D warnings"
  CARGO_INCREMENTAL: 0
  CARGO_PROFILE_DEV_DEBUG: 0
  CARGO_PROFILE_TEST_DEBUG: 0
  CROSS_CONTAINER_UID: 0

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

jobs:
  test:
    name: test
    runs-on: ${{ matrix.os }}
    timeout-minutes: 30
    strategy:
      fail-fast: false
      matrix:
        toolchain: [stable]
        target:
          - x86_64-unknown-linux-gnu
          - x86_64-unknown-linux-musl
          - aarch64-unknown-linux-gnu
          - aarch64-unknown-linux-musl
          - x86_64-apple-darwin
          - x86_64-pc-windows-msvc
        include:
          - os: ubuntu-latest
            target: x86_64-unknown-linux-gnu
            binary: x86-64
            cargo-tool: cargo
            run-integration-tests: true
          - os: ubuntu-latest
            target: x86_64-unknown-linux-musl
            binary: x86-64
            cargo-tool: cross
            run-integration-tests: true
          - os: ubuntu-latest
            target: aarch64-unknown-linux-gnu
            binary: aarch64
            cargo-tool: cross
            run-integration-tests: false # Cannot run aarch64 binaries on x86_64
          - os: ubuntu-latest
            target: aarch64-unknown-linux-musl
            binary: aarch64
            cargo-tool: cross
            run-integration-tests: false # Cannot run aarch64 binaries on x86_64
          - os: macos-15-intel # intel
            target: x86_64-apple-darwin
            binary: x86_64
            cargo-tool: cargo
            run-integration-tests: true
          - os: macos-latest # aarch64
            toolchain: stable
            target: aarch64-apple-darwin
            binary: arm64
            cargo-tool: cargo
            run-integration-tests: true
          - os: windows-2022
            target: x86_64-pc-windows-msvc
            binary: x86-64
            cargo-tool: cargo
            run-integration-tests: true
    steps:
      - name: Checkout repository
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          persist-credentials: false

      - name: Install musl-tools incl. musl-gcc
        uses: awalsh128/cache-apt-pkgs-action@5513791f75b039e2a79653b1a92238d3fb8d99b4 # v1.6.2
        with:
          # musl-tools provide `musl-gcc` which is required for `ring` which is required for `rustls` et al.
          packages: musl-tools
          version: 1.1
        if: ${{ matrix.target == 'x86_64-unknown-linux-musl'}}

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
        with:
          toolchain: ${{ matrix.toolchain }}
          target: ${{ matrix.target }}

      - name: Install Erlang
        uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
        with:
          otp-version: "29"
          elixir-version: "1.19"
          rebar3-version: "3"

      - name: Setup Node
        uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
        with:
          node-version: "25"

      - name: Setup Deno
        uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4
        with:
          deno-version: "2.2"

      - name: Setup Bun
        uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
        with:
          bun-version: "1.2"

      - name: Handle Rust dependencies caching
        uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
        with:
          key: v1-${{ matrix.target }}

      - name: Install Gleam
        uses: clechasseur/rs-cargo@b897b7d2f083c6274295825fdbd5a39b825c3f82 # v5.0.6
        with:
          command: install
          args: "--path gleam-bin --target ${{ matrix.target }} --debug --locked --force"
          tool: ${{ matrix.cargo-tool }}
        if: ${{ matrix.run-integration-tests }}

      - name: Verify binary architecture
        shell: bash
        run: |
          BINARY_PATH="${CARGO_HOME}/bin/gleam"
          if [[ "${{ matrix.target }}" == *"windows"* ]]; then
            BINARY_PATH="${BINARY_PATH}.exe"
          fi

          if ! file -b "$BINARY_PATH" | grep -q "${{ matrix.binary }}"; then
            echo "error: Architecture mismatch"
            echo "Expected architecture: '${{ matrix.binary }}'"
            echo "Found binary type: '$(file -b "$BINARY_PATH")'"
            exit 1
          fi
          echo "ok: Architecture match"
        if: ${{ matrix.run-integration-tests }}

      - name: Run tests
        uses: clechasseur/rs-cargo@b897b7d2f083c6274295825fdbd5a39b825c3f82 # v5.0.6
        with:
          command: test
          # We only want to run `test-{output,commands}` when running integration tests.
          # There's a caveat though: when `cargo-tool` is `cross` it uses a container
          # and would result in these integration tests failing due to not finding
          # the escript binary. So, in case `cargo-tool != cargo` we'll skip the
          # `test-output` tests as well.
          args: >-
            --workspace
            --target ${{ matrix.target }}
            ${{ ((matrix.run-integration-tests && matrix.cargo-tool == 'cargo')
                  && ' ')
                  || '--exclude test-output --exclude test-commands' }}
          tool: ${{ matrix.cargo-tool }}

      - name: test/project_erlang (non-windows)
        run: |
          gleam run && cd src && gleam run && cd ..
          gleam check
          gleam test && cd src && gleam test && cd ..
          gleam docs build
        working-directory: ./test/project_erlang
        if: ${{ runner.os != 'Windows' && matrix.run-integration-tests }}

      - name: test/project_erlang (windows)
        run: |
          gleam run && cd src && gleam run && cd ..
          gleam check
          gleam test && cd src && gleam test && cd ..
          gleam docs build
        working-directory: ./test/project_erlang_windows
        if: ${{ runner.os == 'Windows' && matrix.run-integration-tests }}

      - name: test/project_erlang export erlang-shipment (non-windows)
        run: |
          gleam export erlang-shipment
          ./build/erlang-shipment/entrypoint.sh run
        working-directory: ./test/project_erlang
        if: ${{ runner.os != 'Windows' && matrix.run-integration-tests }}

      - name: test/project_erlang export erlang-shipment (windows)
        run: |
          gleam export erlang-shipment
          .\build\erlang-shipment\entrypoint.ps1 run
        working-directory: ./test/project_erlang_windows
        if: ${{ runner.os == 'Windows' && matrix.run-integration-tests }}

      - name: test/project_erlang export package-interface (non-windows)
        run: |
          gleam export package-interface --out="interface.json"
          cat interface.json
        working-directory: ./test/project_erlang
        if: ${{ runner.os != 'Windows' && matrix.run-integration-tests }}

      - name: test/project_erlang export package-interface (windows)
        run: |
          gleam export package-interface --out="interface.json"
          cat interface.json
        working-directory: ./test/project_erlang_windows
        if: ${{ runner.os == 'Windows' && matrix.run-integration-tests }}

      - name: test/project_erlang export package-information
        run: |
          gleam export package-information --out="gleam.json"
          cat gleam.json
        working-directory: ./test/project_erlang
        if: ${{ matrix.run-integration-tests }}

      - name: test/external_only_javascript
        run: ./test.sh
        working-directory: ./test/external_only_javascript
        if: ${{ matrix.run-integration-tests }}
        env:
          GLEAM_COMMAND: gleam

      - name: test/external_only_erlang
        run: ./test.sh
        working-directory: ./test/external_only_erlang
        if: ${{ matrix.run-integration-tests }}
        env:
          GLEAM_COMMAND: gleam

      - name: test/root_package_not_compiled_when_running_dep
        run: ./test.sh
        working-directory: ./test/root_package_not_compiled_when_running_dep
        if: ${{ matrix.run-integration-tests }}
        env:
          GLEAM_COMMAND: gleam

      - name: test/erlang_shipment_no_dev_deps
        run: ./test.sh
        working-directory: ./test/erlang_shipment_no_dev_deps
        if: ${{ matrix.run-integration-tests }}
        env:
          GLEAM_COMMAND: gleam

      - name: test/project_javascript
        run: |
          gleam run
          gleam check
          gleam test
          gleam docs build
        working-directory: ./test/project_javascript
        if: ${{ matrix.run-integration-tests }}

      - name: test/project_path_deps
        run: |
          gleam update
          gleam check
        working-directory: ./test/project_path_deps/project_a
        if: ${{ matrix.run-integration-tests }}

      - name: test/project_git_deps
        run: |
          gleam update
          gleam check
        working-directory: ./test/project_git_deps
        if: ${{ matrix.run-integration-tests }}

      - name: test/project_git_deps_path
        run: ./test.sh
        working-directory: ./test/project_git_deps_path
        if: ${{ matrix.run-integration-tests }}
        env:
          GLEAM_COMMAND: gleam

      - name: Test project generation
        run: |
          gleam new lib_project
          cd lib_project
          gleam run
          gleam test

          # Test adding of deps
          gleam add exception    # No specifier
          gleam add gleam_http@4 # Version specifier
          gleam test

          # Test documentation generation
          gleam docs build

          # Assert that module metadata has been written
          ls build/dev/erlang/lib_project/_gleam_artefacts/lib_project.cache

          # Assert that HTML docs and their assets have been written
          ls build/dev/docs/lib_project/index.html
          ls build/dev/docs/lib_project/lib_project.html
          ls build/dev/docs/lib_project/css/atom-one-light.min.css
          ls build/dev/docs/lib_project/css/atom-one-dark.min.css
          ls build/dev/docs/lib_project/css/index.css
          ls build/dev/docs/lib_project/js/highlight.min.js
          ls build/dev/docs/lib_project/js/highlightjs-gleam.js
          ls build/dev/docs/lib_project/js/highlightjs-erlang.min.js
          ls build/dev/docs/lib_project/js/highlightjs-elixir.min.js
          ls build/dev/docs/lib_project/js/highlightjs-javascript.min.js
          ls build/dev/docs/lib_project/js/highlightjs-typescript.min.js
          ls build/dev/docs/lib_project/js/lunr.min.js
          ls build/dev/docs/lib_project/js/index.js
          ls build/dev/docs/lib_project/fonts/karla-v23-bold-latin-ext.woff2
          ls build/dev/docs/lib_project/fonts/karla-v23-bold-latin.woff2
          ls build/dev/docs/lib_project/fonts/karla-v23-regular-latin-ext.woff2
          ls build/dev/docs/lib_project/fonts/karla-v23-regular-latin.woff2
          ls build/dev/docs/lib_project/fonts/ubuntu-mono-v15-regular-cyrillic-ext.woff2
          ls build/dev/docs/lib_project/fonts/ubuntu-mono-v15-regular-cyrillic.woff2
          ls build/dev/docs/lib_project/fonts/ubuntu-mono-v15-regular-greek-ext.woff2
          ls build/dev/docs/lib_project/fonts/ubuntu-mono-v15-regular-greek.woff2
          ls build/dev/docs/lib_project/fonts/ubuntu-mono-v15-regular-latin-ext.woff2
          ls build/dev/docs/lib_project/fonts/ubuntu-mono-v15-regular-latin.woff2
          ls build/dev/docs/lib_project/fonts/ubuntu-mono-v15-regular-box.woff2
        if: ${{ matrix.run-integration-tests }}

  test-wasm:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - name: Checkout repository
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          persist-credentials: false

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
        with:
          toolchain: stable
          target: wasm32-unknown-unknown

      - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
        with:
          node-version: "25"

      - name: Install wasm-pack
        run: |
          curl -sSL https://rustwasm.github.io/wasm-pack/installer/init.sh | sh

      - name: Run wasm tests
        run: wasm-pack test --node compiler-wasm

  rustfmt:
    name: rustfmt
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - name: Checkout repository
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          persist-credentials: false

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
        with:
          toolchain: stable
          components: rustfmt

      - run: cargo fmt --all -- --check

  validate:
    name: validate
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - name: Checkout repository
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          persist-credentials: false

      - name: Ensure no merge commits
        uses: NexusPHP/no-merge-commits@109a3342781ddcf26b2fa739f29e8ee9f68b7d8e # v2.4.1
        if: github.event_name == 'pull_request'
        with:
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
        with:
          toolchain: stable

      - name: Install cargo-deny
        run: |
          set -e
          curl -L https://github.com/EmbarkStudios/cargo-deny/releases/download/0.18.6/cargo-deny-0.18.6-x86_64-unknown-linux-musl.tar.gz | tar xzf -
          mv cargo-deny-*-x86_64-unknown-linux-musl/cargo-deny cargo-deny
          echo `pwd` >> $GITHUB_PATH

      - name: Validate deps
        run: cargo deny check

  lint-build:
    name: lint-build
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - name: Checkout repository
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          persist-credentials: false

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
        with:
          toolchain: stable
          components: clippy

      - name: Handle Rust dependencies caching
        uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
        with:
          key: v1-linux-gnu

      - name: Run linter
        run: cargo clippy --workspace

      - run: cargo build

      - name: Upload artifact (Ubuntu)
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: gleam
          path: target/debug/gleam

  test-projects:
    name: test-projects
    needs: lint-build
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - name: Checkout repository
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
        with:
          persist-credentials: false

      - name: Install Deno
        uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4
        with:
          deno-version: v2.x

      - name: Install Bun
        uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0

      - name: Install Erlang
        uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0
        with:
          otp-version: "29"
          elixir-version: "1.19"
          rebar3-version: "3"

      - name: Download Gleam binary from previous job
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: gleam
          path: ./test

      - name: Configure test projects to use Gleam binary
        run: |
          echo $PWD/ >> $GITHUB_PATH
          chmod +x ./gleam
          sed -i 's/cargo run --quiet --/gleam/' */Makefile
          sed -i 's/cargo run --/gleam/' */Makefile
        working-directory: ./test

      - name: test/language Erlang
        run: make clean erlang
        working-directory: ./test/language

      - name: test/language JavaScript with NodeJS
        run: make clean nodejs
        working-directory: ./test/language

      - name: test/language JavaScript with Deno
        run: make clean deno
        working-directory: ./test/language

      - name: test/language JavaScript with Bun
        run: make clean bun
        working-directory: ./test/language

      - name: test/compile_package0
        run: make
        working-directory: ./test/compile_package0

      - name: test/compile_package1
        run: make
        working-directory: ./test/compile_package1

      - name: Test JavaScript prelude
        run: make
        working-directory: ./test/javascript_prelude

      - name: Test generated TypeScript declarations
        run: make test
        working-directory: ./test/typescript_declarations

      - name: Test export of hex tarball
        run: make test
        working-directory: ./test/hextarball

      - name: test/running_modules
        run: make test-all
        working-directory: ./test/running_modules

      - name: test/multi_namespace
        run: ./test.sh
        working-directory: ./test/multi_namespace

      - name: test/multi_namespace_not_top_level
        run: ./test.sh
        working-directory: ./test/multi_namespace_not_top_level

      - name: Test FFI in subdirectories
        run: make
        working-directory: ./test/subdir_ffi

      - name: test/unicode_path
        run: make
        working-directory: ./test/unicode_path ⭐

      - name: test/assert
        run: make test-all
        working-directory: ./test/assert

      - name: Test publishing with default main
        run: ./test.sh
        working-directory: ./test/publishing_default_main

      - name: Test publishing with src symlink escape
        run: ./test.sh
        working-directory: ./test/publishing_src_symlink_escape

      - name: Test publishing with priv symlink escape
        run: ./test.sh
        working-directory: ./test/publishing_priv_symlink_escape
