{"id":38892427,"url":"https://github.com/int128/parallel-test-action","last_synced_at":"2026-05-22T22:00:44.233Z","repository":{"id":258057666,"uuid":"868714430","full_name":"int128/parallel-test-action","owner":"int128","description":"Parallel testing in GitHub Actions","archived":false,"fork":false,"pushed_at":"2026-05-16T22:14:23.000Z","size":79807,"stargazers_count":4,"open_issues_count":5,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-05-16T23:39:18.945Z","etag":null,"topics":["github-actions","parallel-testing","testing"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/int128.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-10-07T03:46:32.000Z","updated_at":"2026-05-16T22:13:52.000Z","dependencies_parsed_at":"2024-10-27T11:41:09.877Z","dependency_job_id":null,"html_url":"https://github.com/int128/parallel-test-action","commit_stats":null,"previous_names":["int128/parallel-test-action"],"tags_count":111,"template":false,"template_full_name":"int128/typescript-action","purl":"pkg:github/int128/parallel-test-action","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/int128%2Fparallel-test-action","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/int128%2Fparallel-test-action/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/int128%2Fparallel-test-action/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/int128%2Fparallel-test-action/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/int128","download_url":"https://codeload.github.com/int128/parallel-test-action/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/int128%2Fparallel-test-action/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33372736,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-22T21:56:13.512Z","status":"ssl_error","status_checked_at":"2026-05-22T21:56:10.769Z","response_time":265,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["github-actions","parallel-testing","testing"],"created_at":"2026-01-17T14:55:45.462Z","updated_at":"2026-05-22T22:00:44.198Z","avatar_url":"https://github.com/int128.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# parallel-test-action [![ts](https://github.com/int128/parallel-test-action/actions/workflows/ts.yaml/badge.svg)](https://github.com/int128/parallel-test-action/actions/workflows/ts.yaml)\n\nThis action distributes the test files to the shards based on the estimated time from the test reports.\nYou can reduce the time of tests by parallel testing.\n\n## Overview\n\nThis action distibutes the test files to the shards based on the estimated time.\nHere is the example of the distribution:\n\n```mermaid\ngraph TB\n  subgraph Test Files\n    TF1[Test File #1]\n    TF2[Test File #2]\n    TF3[Test File #3]\n    TF4[Test File #4]\n    TF5[Test File #5]\n  end\n  subgraph Shard Files\n    S1[Shard #1]\n    S2[Shard #2]\n    S3[Shard #3]\n  end\n  subgraph J3[Job #3]\n    T3[Testing Framework]\n  end\n  subgraph J2[Job #2]\n    T2[Testing Framework]\n  end\n  subgraph J1[Job #1]\n    T1[Testing Framework]\n  end\n  TF1 --\u003e S1\n  TF2 --\u003e S1\n  TF3 --\u003e S2\n  TF4 --\u003e S3\n  TF5 --\u003e S3\n  S1 --\u003e T1\n  S2 --\u003e T2\n  S3 --\u003e T3\n```\n\nEach shard should contain the test files with the similar estimated time.\nThis action uses the greedy algorithm.\n\nYou need to upload the test reports as artifacts on the default branch.\nThis action estimate the time of each test file using the test reports.\nIf a test file is not found in the test reports, this action assumes the average time of all test files.\nIf no test report is given, this action falls back to the round-robin distribution.\n\n## Examples\n\n### Vitest or Jest\n\nHere is an example workflow to run Vitest or Jest in parallel.\n\n```yaml\njobs:\n  test:\n    strategy:\n      fail-fast: false\n      matrix:\n        shard-id: [1, 2, 3]\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: int128/parallel-test-action@v1\n        id: parallel-test\n        with:\n          test-files: \"tests/**/*.test.ts\"\n          test-report-artifact-name-prefix: test-report-\n          test-report-branch: main\n          shard-count: ${{ strategy.job-total }}\n      - uses: actions/setup-node@v4\n      # ...snip...\n      - run: xargs pnpm run test -- \u003c \"$SHARD_FILE\"\n        env:\n          SHARD_FILE: ${{ steps.parallel-test.outputs.shards-directory }}/${{ matrix.shard-id }}\n      - uses: actions/upload-artifact@v4\n        with:\n          name: test-report-${{ matrix.shard-id }}\n          path: junit.xml\n```\n\nThis action requires `file` attribute of the test report.\nFor Vitest, see [vitest.config.ts](vitest.config.ts) for the configuration to generate the test report.\nFor Jest, you can use [jest-junit](https://github.com/jest-community/jest-junit).\n\n### RSpec\n\nHere is an example workflow to run RSpec in parallel.\n\n```yaml\njobs:\n  test:\n    strategy:\n      fail-fast: false\n      matrix:\n        shard-id: [1, 2, 3]\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: int128/parallel-test-action@v1\n        id: parallel-test\n        with:\n          test-files: \"spec/**/*_spec.rb\"\n          test-report-artifact-name-prefix: test-report-\n          test-report-branch: main\n          shard-count: ${{ strategy.job-total }}\n      - uses: ruby/setup-ruby@v1\n      # ...snip...\n      - run: xargs bundle exec rspec --format RspecJunitFormatter --out rspec.xml \u003c \"$SHARD_FILE\"\n        env:\n          SHARD_FILE: ${{ steps.parallel-test.outputs.shards-directory }}/${{ matrix.shard-id }}\n      - uses: actions/upload-artifact@v4\n        with:\n          name: test-report-${{ matrix.shard-id }}\n          path: rspec.xml\n```\n\nYou can generate a test report using [rspec_junit_formatter](https://github.com/sj26/rspec_junit_formatter).\n\n## How it works\n\n### Action overview\n\nHere is the inputs and outputs of this action:\n\n- Test Files (input)\n  - This action finds the test files specified by a glob pattern (e.g. `tests/**/*.test.ts`).\n- Test Reports (input)\n  - This action finds the last success workflow run of the specified branch, and downloads the test reports.\n  - A test report should contain the duration of each test case.\n- Shard Files (output)\n  - This action generates the shard files based on the estimated time of each test file.\n  - A shard file contains the list of test files.\n  - Each job should run the tests in the corresponding shard file. For example, job #1 runs the tests in shard #1.\n\nHere is the flow of test job:\n\n```mermaid\ngraph TB\n  LTR[Test Report #1...#N of the last workflow run] --\u003e A\n  subgraph Job #1\n    A[parallel-test-action]\n    WT1[Test Files in the working directory] --\u003e A\n    subgraph SF[Shard Files]\n      S1[Shard #1]\n      S3[Shard ... #N]\n    end\n    A --\u003e SF\n    S1 --\u003e T[Testing Framework]\n  end\n```\n\n### Workflow overview\n\nThe test workflow runs the test jobs in parallel.\nEach job should process the corresponding shard.\n\nWhen this action is run in parallel jobs, each job may generate the different shard files.\nTo avoid the race condition, this action acquires the lock as follows:\n\n1. The first job uploads the shards as an artifact.\n   This operation is atomic since GitHub Actions Artifact rejects the same name of artifact.\n2. The subsequent jobs download the shards artifact and use it.\n   They discard their generated shards.\n\nHere is the structure of test workflow:\n\n```mermaid\ngraph TB\n  subgraph Future workflow run\n    N[parallel-test-action]\n  end\n  TR1 -.-\u003e N\n  TR2 -.-\u003e N\n  subgraph Current workflow run\n    A1 --Upload--\u003e S[Shards] --Download--\u003e A2\n    subgraph J2[Subsequent Job #j]\n      A2[parallel-test-action] --\u003e T2[Testing Framework] --\u003e TR2[Test Report #j]\n    end\n    subgraph J1[First Job #i]\n      A1[parallel-test-action] --\u003e T1[Testing Framework] --\u003e TR1[Test Report #i]\n    end\n  end\n  subgraph Last workflow run\n    LTR1[Test Report #1] --Download--\u003e A1\n    LTR2[Test Report ... #N] --Download--\u003e A1\n  end\n```\n\n## Specification\n\nHere is the typical workflow to run the parallel test jobs.\n\n```yaml\non:\n  push:\n    branches:\n      - main\n  pull_request:\n\njobs:\n  test:\n    strategy:\n      fail-fast: false\n      matrix:\n        shard-id: [1, 2, 3] # Shard ID starts from #1\n    runs-on: ubuntu-latest\n    steps:\n      # (1) Checkout the repository.\n      - uses: actions/checkout@v4\n\n      # (2) Distribute the test files to the shards.\n      - uses: int128/parallel-test-action@v1\n        id: parallel-test\n        with:\n          test-files: \"**/*\" # Glob pattern of your test files\n          test-report-artifact-name-prefix: test-report- # Find the test reports of this name\n          test-report-branch: main # Find the test reports from the main branch\n          shard-count: ${{ strategy.job-total }}\n\n      # (3) Run your testing framework.\n      - run: xargs your-testing-framework \u003c \"$SHARD_FILE\"\n        env:\n          SHARD_FILE: ${{ steps.parallel-test.outputs.shards-directory }}/${{ matrix.shard-id }}\n\n      # (4) Upload the test report as an artifact.\n      - uses: actions/upload-artifact@v4\n        with:\n          name: test-report-${{ matrix.shard-id }}\n          path: junit.xml\n```\n\nSteps:\n\n1. Checkout the repository.\n   This action depends on the working directory to generate the list of test files.\n2. Distribute the test files to the shards.\n   It generates the list of test files for each shard.\n3. Run your testing framework.\n   It should accept the list of test files to run.\n   It should also generate the test report.\n4. Upload the test report as an artifact.\n   The artifact will be used in the future workflow runs.\n\n### Inputs\n\n| Name                               | Default        | Description                              |\n| ---------------------------------- | -------------- | ---------------------------------------- |\n| `working-directory`                | `.`            | Working directory                        |\n| `test-files`                       | (required)     | Glob pattern of test files               |\n| `test-report-artifact-name-prefix` | (required)     | Prefix of the test report artifact name  |\n| `test-report-branch`               | (required)     | Branch to find the test report artifacts |\n| `shard-count`                      | (required)     | Number of shards                         |\n| `shards-artifact-name`             | (\\*1)          | Name of the shards artifact              |\n| `token`                            | (github.token) | GitHub token                             |\n\n(\\*1) The value of `shards-artifact-name` must be same in the parallel jobs.\nThe default value is `parallel-test-shards--${{ github.job }}`.\nFor above example, the shards artifact is uploaded as `parallel-test-shards--test`.\n\n### Outputs\n\n| Name               | Description                        |\n| ------------------ | ---------------------------------- |\n| `shards-directory` | Directory to store the shard files |\n\nThis action writes the shard files to the temporary directory.\nThe shards directory looks like:\n\n```\n/home/runner/work/_temp/parallel-test-action-*/shards/1\n/home/runner/work/_temp/parallel-test-action-*/shards/2\n/home/runner/work/_temp/parallel-test-action-*/shards/3\n...\n```\n\nThe shard ID starts from 1.\n\nEach shard file contains the list of test files.\nFor example,\n\n```\ntests/foo.test.ts\ntests/bar.test.ts\ntests/baz.test.ts\n...\n```\n\nYour testing framework should run the test files in the shard file.\nYou can construct the command by `xargs`, for example:\n\n```sh\nxargs your_testing_framework \u003c '${{ steps.parallel-test.outputs.shards-directory }}/${{ matrix.shard-id }}'\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fint128%2Fparallel-test-action","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fint128%2Fparallel-test-action","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fint128%2Fparallel-test-action/lists"}