actions/run-tests
2026-05-15 15:46:55 +02:00
..
action.yml feat: automated tests 2026-05-15 11:48:04 +02:00
README.md doc: update examples with confirmed working settings 2026-05-15 15:46:55 +02:00

Run Tests

Language-agnostic composite action that executes a project's test suite with optional setup and failure-artifact upload. Designed for Forgejo Actions (works on GitHub Actions too).

Inputs

Input Required Default Description
command yes Test command to execute (multiline allowed)
setup no "" Setup commands run before tests (install deps, build, etc.)
working-directory no . Directory to run setup and command in
shell no bash Shell for setup and command
artifacts-path no "" Glob of paths to upload on failure. Empty disables upload.
artifacts-name no test-artifacts Name of the uploaded artifact bundle

Behavior

  1. Runs setup if provided. Non-zero exit fails the job immediately.
  2. Runs command. Exit code is captured but not yet propagated.
  3. If command failed and artifacts-path is set, uploads matching files.
  4. Re-exits with failure if command failed.

This ordering ensures artifacts are always uploaded on failure, even though the job ultimately fails.

Workflow shape

This Forgejo instance is hosted on a subpath (bitfreedom.net/code/...), and Forgejo's action resolver does not handle URL-form uses: references against subpath instances. The workflows below therefore:

  1. Run inside a container: so the toolchain is predictable across runners.
  2. Clone the repository under test manually with the job token, in place of actions/checkout.
  3. Clone this actions repo into a local directory and reference the action with uses: ./.actions/run-tests, in place of uses: https://.../run-tests@v1.

The manual checkout uses github.event.pull_request.head.sha, so it assumes on: [pull_request]. For on: push, swap that for github.sha (or github.event.head_commit.id). The same workflow files run on GitHub unchanged.

Usage

Each example below is a complete workflow — drop it into .forgejo/workflows/test.yml (or .github/workflows/test.yml) and adjust runs-on to match a label one of your runners is registered with. See Choosing a runner below.

Node.js

name: PR Tests
on: [pull_request]
jobs:
  test:
    runs-on: docker-amd64
    container:
      image: node:20-slim
    steps:
      - name: Install system deps
        run: |
          apt-get update
          apt-get install -y --no-install-recommends git ca-certificates
          rm -rf /var/lib/apt/lists/*
      - name: Checkout
        run: |
          git config --global --add safe.directory "$PWD"
          git clone --depth=1 \
            "https://oauth2:${{ github.token }}@bitfreedom.net/code/${{ github.repository }}.git" .
          git fetch --depth=1 origin "+${{ github.event.pull_request.head.sha }}:pr"
          git checkout pr
      - name: Fetch action source
        run: |
          git clone --depth=1 --branch v1 \
            "https://oauth2:${{ github.token }}@bitfreedom.net/code/nomyo-ai/actions.git" \
            ./.actions
      - uses: ./.actions/run-tests
        with:
          setup: npm ci
          command: npm test
          artifacts-path: |
            coverage/**
            junit.xml

Python (pytest)

name: PR Tests
on: [pull_request]
jobs:
  test:
    runs-on: docker-amd64
    container:
      image: python:3.12-slim
    steps:
      - name: Install system deps
        run: |
          apt-get update
          apt-get install -y --no-install-recommends git ca-certificates
          rm -rf /var/lib/apt/lists/*
      - name: Checkout
        run: |
          git config --global --add safe.directory "$PWD"
          git clone --depth=1 \
            "https://oauth2:${{ github.token }}@bitfreedom.net/code/${{ github.repository }}.git" .
          git fetch --depth=1 origin "+${{ github.event.pull_request.head.sha }}:pr"
          git checkout pr
      - name: Fetch action source
        run: |
          git clone --depth=1 --branch v1 \
            "https://oauth2:${{ github.token }}@bitfreedom.net/code/nomyo-ai/actions.git" \
            ./.actions
      - uses: ./.actions/run-tests
        with:
          setup: |
            python -m pip install --upgrade pip
            pip install -e .[test]
          command: pytest --junitxml=report.xml
          artifacts-path: report.xml

Rust

name: PR Tests
on: [pull_request]
jobs:
  test:
    runs-on: docker-amd64
    container:
      image: rust:1
    steps:
      - name: Checkout
        run: |
          git config --global --add safe.directory "$PWD"
          git clone --depth=1 \
            "https://oauth2:${{ github.token }}@bitfreedom.net/code/${{ github.repository }}.git" .
          git fetch --depth=1 origin "+${{ github.event.pull_request.head.sha }}:pr"
          git checkout pr
      - name: Fetch action source
        run: |
          git clone --depth=1 --branch v1 \
            "https://oauth2:${{ github.token }}@bitfreedom.net/code/nomyo-ai/actions.git" \
            ./.actions
      - uses: ./.actions/run-tests
        with:
          command: cargo test --all

Java (Gradle)

name: PR Tests
on: [pull_request]
jobs:
  test:
    runs-on: docker-amd64
    container:
      image: gradle:8-jdk21
    steps:
      - name: Checkout
        run: |
          git config --global --add safe.directory "$PWD"
          git clone --depth=1 \
            "https://oauth2:${{ github.token }}@bitfreedom.net/code/${{ github.repository }}.git" .
          git fetch --depth=1 origin "+${{ github.event.pull_request.head.sha }}:pr"
          git checkout pr
      - name: Fetch action source
        run: |
          git clone --depth=1 --branch v1 \
            "https://oauth2:${{ github.token }}@bitfreedom.net/code/nomyo-ai/actions.git" \
            ./.actions
      - uses: ./.actions/run-tests
        with:
          setup: gradle --version
          command: gradle test
          artifacts-path: |
            **/build/reports/tests/**
            **/build/test-results/**/*.xml

Java (Maven)

name: PR Tests
on: [pull_request]
jobs:
  test:
    runs-on: docker-amd64
    container:
      image: maven:3-eclipse-temurin-21
    steps:
      - name: Checkout
        run: |
          git config --global --add safe.directory "$PWD"
          git clone --depth=1 \
            "https://oauth2:${{ github.token }}@bitfreedom.net/code/${{ github.repository }}.git" .
          git fetch --depth=1 origin "+${{ github.event.pull_request.head.sha }}:pr"
          git checkout pr
      - name: Fetch action source
        run: |
          git clone --depth=1 --branch v1 \
            "https://oauth2:${{ github.token }}@bitfreedom.net/code/nomyo-ai/actions.git" \
            ./.actions
      - uses: ./.actions/run-tests
        with:
          setup: mvn --version
          command: mvn -B test
          artifacts-path: |
            **/target/surefire-reports/**
            **/target/failsafe-reports/**

Multi-line command

Any of the examples above can run multiple commands in a single step by passing a YAML block scalar to command:

      - uses: ./.actions/run-tests
        with:
          command: |
            make lint
            make test
            make integration-test

Choosing a runner

runs-on must match the labels a runner was registered with — there is no fuzzy match. If your runners are registered as docker-amd64 and docker-arm64, then runs-on: docker will queue forever waiting for a runner that doesn't exist.

Single architecture

Pick the label that matches your runner:

jobs:
  test:
    runs-on: docker-amd64

Both architectures (matrix)

Run the same job on every arch in parallel. Useful when you ship binaries, link against native libraries, or want to catch arch-specific bugs early:

name: PR Tests
on: [pull_request]
jobs:
  test:
    strategy:
      fail-fast: false
      matrix:
        arch: [docker-amd64, docker-arm64]
    runs-on: ${{ matrix.arch }}
    container:
      image: python:3.12-slim
    steps:
      - name: Install system deps
        run: |
          apt-get update
          apt-get install -y --no-install-recommends git ca-certificates
          rm -rf /var/lib/apt/lists/*
      - name: Checkout
        run: |
          git config --global --add safe.directory "$PWD"
          git clone --depth=1 \
            "https://oauth2:${{ github.token }}@bitfreedom.net/code/${{ github.repository }}.git" .
          git fetch --depth=1 origin "+${{ github.event.pull_request.head.sha }}:pr"
          git checkout pr
      - name: Fetch action source
        run: |
          git clone --depth=1 --branch v1 \
            "https://oauth2:${{ github.token }}@bitfreedom.net/code/nomyo-ai/actions.git" \
            ./.actions
      - uses: ./.actions/run-tests
        with:
          command: ./run-tests.sh

fail-fast: false lets the other arch finish even if one fails, so you see both results.

Does the architecture matter for the tests?

  • Pure JVM, Python, or Node with no native dependencies: only execution speed differs.
  • Native code (Rust, C/C++, JNI, Python wheels like numpy/cryptography, Node modules built with node-gyp): the compiled artifact is arch-specific. Same source, different binary — running the matrix exercises both.
  • Container images pulled during setup: must publish a manifest for the runner's arch. Most official images do; some niche ones are amd64-only and will fail on arm64.

Making it a merge gate

This action alone does not block merges. To enforce passing tests before merge on Forgejo:

  1. Repo → Settings → Branches → Protected Branches
  2. Add a rule for main (or your default branch)
  3. Enable Require status checks to pass before merging
  4. Add the workflow job name (e.g. test) to the required checks list

After the workflow has run at least once on a PR, the check name will appear in the dropdown.

Notes

  • runs-on must match a label your runner was registered with — see Choosing a runner. The GitHub-style ubuntu-latest typically does not exist on Forgejo.
  • This Forgejo instance is hosted on a subpath, so URL-form uses: (e.g. uses: https://bitfreedom.net/code/...@v1) does not resolve. The examples clone the repository under test and the actions repo manually, then reference the action by local path. See Workflow shape.
  • The failure-artifact upload uses forgejo/upload-artifact, which is API-compatible with actions/upload-artifact and works on both Forgejo and GitHub.