From 92d07a11e1b7c127245db6d5f4d910e394463c22 Mon Sep 17 00:00:00 2001 From: Dave Lucia Date: Fri, 10 Jul 2026 15:08:32 -0400 Subject: [PATCH 1/2] Add GitHub Actions CI workflow Runs the test suite, format check, unused-deps check, and warnings-as-errors compile across Elixir 1.19/OTP 28 and 1.20/OTP 29. The Oils-format conformance spec suite (which tracks known gaps against reference Bash) runs in a separate, non-blocking job that reports its pass/fail count to the job summary. --- .github/workflows/ci.yml | 149 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2929c53 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,149 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Test + +on: + push: + branches: ["main"] + pull_request: + +permissions: + contents: read + +jobs: + build: + name: Build and test (${{ matrix.elixir }} / OTP ${{ matrix.otp }}) + runs-on: ubuntu-latest + env: + MIX_ENV: test + strategy: + fail-fast: false + matrix: + include: + - elixir: "1.19.0" + otp: "28.0" + bootstrap_beam: false + lint: true + # OTP 29 RC's :httpc can't reach builds.hex.pm, so we tell setup-beam + # to skip Hex/Rebar autoinstall and curl them ourselves below. + - elixir: "1.20.0" + otp: "29.0" + bootstrap_beam: true + lint: false + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ matrix.elixir }} + otp-version: ${{ matrix.otp }} + version-type: strict + install-hex: ${{ !matrix.bootstrap_beam }} + install-rebar: ${{ !matrix.bootstrap_beam }} + + - name: Bootstrap Hex/Rebar via curl (OTP 29 RC :httpc workaround) + if: matrix.bootstrap_beam + run: | + curl -fsSL --retry 3 -o /tmp/hex.ez \ + https://builds.hex.pm/installs/1.19.0/hex-2.4.2-otp-28.ez + mix archive.install /tmp/hex.ez --force + curl -fsSL --retry 3 -o /tmp/rebar3 \ + https://github.com/erlang/rebar3/releases/download/3.25.1/rebar3 + chmod +x /tmp/rebar3 + mix local.rebar rebar3 /tmp/rebar3 --force + + - name: Restore dependencies cache + uses: actions/cache@v4 + with: + path: deps + key: ${{ runner.os }}-elixir${{ matrix.elixir }}-otp${{ matrix.otp }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-elixir${{ matrix.elixir }}-otp${{ matrix.otp }}-mix- + + - name: Restore build cache + uses: actions/cache@v4 + with: + path: _build + key: ${{ runner.os }}-elixir${{ matrix.elixir }}-otp${{ matrix.otp }}-build-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-elixir${{ matrix.elixir }}-otp${{ matrix.otp }}-build- + + - name: Install dependencies + run: mix deps.get + + - name: Check formatting + if: matrix.lint + run: mix format --check-formatted + + - name: Check for unused dependencies + if: matrix.lint + run: mix deps.unlock --check-unused + + - name: Compile (warnings as errors) + run: mix compile --warnings-as-errors --force + + - name: Run tests + # The `spec` conformance suite is run separately (see spec-suite job) + # because it tracks known gaps against reference Bash behavior and + # should not block PRs. + run: mix test --exclude spec + + spec-suite: + name: Conformance spec suite + runs-on: ubuntu-latest + # Reports the pass/fail count for the Oils-format conformance specs but + # does not block PRs — these track known gaps against reference Bash. + continue-on-error: true + env: + MIX_ENV: test + strategy: + matrix: + include: + - elixir: "1.19.0" + otp: "28.0" + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ matrix.elixir }} + otp-version: ${{ matrix.otp }} + version-type: strict + + - name: Restore dependencies cache + uses: actions/cache@v4 + with: + path: deps + key: ${{ runner.os }}-elixir${{ matrix.elixir }}-otp${{ matrix.otp }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-elixir${{ matrix.elixir }}-otp${{ matrix.otp }}-mix- + + - name: Restore build cache + uses: actions/cache@v4 + with: + path: _build + key: ${{ runner.os }}-elixir${{ matrix.elixir }}-otp${{ matrix.otp }}-build-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-elixir${{ matrix.elixir }}-otp${{ matrix.otp }}-build- + + - name: Install dependencies + run: mix deps.get + + - name: Run conformance spec suite + run: | + mix test --only spec 2>&1 | tee spec-output.txt + summary=$(grep -E '[0-9]+ tests?, [0-9]+ failures' spec-output.txt | tail -1) + { + echo "## Conformance spec suite" + echo "" + echo "\`\`\`" + echo "$summary" + echo "\`\`\`" + } >> "$GITHUB_STEP_SUMMARY" + # Always succeed so the count is reported; the job is non-blocking. + exit 0 From f8b74258535fc7679eae487b71915605ebc21ee8 Mon Sep 17 00:00:00 2001 From: Dave Lucia Date: Fri, 10 Jul 2026 15:42:16 -0400 Subject: [PATCH 2/2] Fix noclobber test to quote redirect target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test interpolated the tmp_dir path into an unquoted redirect target. On Elixir 1.19+, ExUnit's tmp_dir name includes the test description verbatim, which for this test contains "(-C)" — and unquoted parentheses are shell-special, so the redirect misparsed (exit 127, no error output) rather than triggering noclobber. Real Bash would fail on the unquoted path too. Quote the redirect target so the path is treated literally, and read stderr via the session's persistent collector (flush_session_output/1) rather than the result-embedded collector, which is transferred and cleaned up asynchronously after execution. --- test/bash/builtin/set_options_test.exs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/test/bash/builtin/set_options_test.exs b/test/bash/builtin/set_options_test.exs index ce1177c..24687b4 100644 --- a/test/bash/builtin/set_options_test.exs +++ b/test/bash/builtin/set_options_test.exs @@ -182,17 +182,23 @@ defmodule Bash.Builtin.SetOptionsTest do temp_file = Path.join(tmp_dir, "noclobber_test") File.write!(temp_file, "original content") + # Quote the redirect target: the tmp_dir path can contain shell-special + # characters (e.g. "(" from the test name), which would otherwise break + # parsing of an unquoted redirect target. result = run_script(session, """ set -C - echo "new content" > #{temp_file} + echo "new content" > "#{temp_file}" """) - # Command should have non-zero exit code - assert result.exit_code != 0 + # Command should have a non-zero exit code + assert result.exit_code not in [0, nil] - # Error message should be in stderr - assert String.contains?(get_stderr(result), "cannot overwrite existing file") + # Error message should be in stderr. Read from the session rather than the + # result: the result-embedded collector is transferred to the session's + # persistent collector and cleaned up asynchronously after execution. + {_out, err} = flush_session_output(session) + assert String.contains?(err, "cannot overwrite existing file") # Original content should be preserved assert File.read!(temp_file) == "original content"