Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/actions/nextest/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Run the nextest action fixture
description: Repository-local entry point used by the reusable workflow tests.
inputs:
partition:
default: "1"
description: One-based partition number.
partitions:
default: "1"
description: Total number of partitions.
save-cache:
default: "false"
description: Unused by this small fixture.
runs:
using: composite
steps:
- name: Install Rust
shell: bash
run: rustup toolchain install stable --profile minimal
# Exercise the action from this test checkout, including unmerged changes.
- uses: ./nextest # zizmor: ignore[self-repository]
with:
name: fixture-${{ runner.os }}-${{ inputs.partitions }}
partition: ${{ inputs.partition }}
partitions: ${{ inputs.partitions }}
working-directory: nextest/tests/fixture
arguments: '["--profile", "ci"]'
56 changes: 56 additions & 0 deletions .github/workflows/nextest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Sharded nextest

on:
workflow_call:
inputs:
runs-on:
description: Runner label for each shard.
type: string
default: ubuntu-latest
shards:
description: Number of independent nextest hash partitions.
type: number
default: 1
timeout-minutes:
description: Timeout for each shard.
type: number
default: 20
save-cache:
description: Whether the repository action may save its build cache.
type: boolean
default: true

permissions:
contents: read

jobs:
plan:
name: plan shards
runs-on: ubuntu-slim
outputs:
partitions: ${{ steps.plan.outputs.partitions }}
steps:
- uses: astral-sh/github-actions/nextest/plan@e6fe1b51adba9d0b2c847e503fce90e8fb765188
id: plan
with:
shards: ${{ inputs.shards }}

run:
needs: plan
name: shard ${{ matrix.partition }} of ${{ inputs.shards }}
runs-on: ${{ inputs.runs-on }}
timeout-minutes: ${{ inputs.timeout-minutes }}
strategy:
fail-fast: false
matrix:
partition: ${{ fromJSON(needs.plan.outputs.partitions) }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# This action belongs to the checked-out caller, not this workflow repository.
- uses: ./.github/actions/nextest # zizmor: ignore[self-repository]
with:
partition: ${{ matrix.partition }}
partitions: ${{ inputs.shards }}
save-cache: ${{ inputs.save-cache }}
62 changes: 62 additions & 0 deletions .github/workflows/test-nextest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Test sharded nextest

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

permissions:
contents: read

jobs:
unit:
name: unit tests on ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- run: node --test nextest/tests/options.test.mjs

linux:
name: nextest on linux
uses: $/.github/workflows/nextest.yml
with:
runs-on: ubuntu-latest
shards: 2
macos:
name: nextest on macos
uses: $/.github/workflows/nextest.yml
with:
runs-on: macos-latest
shards: 2
windows:
name: nextest on windows
uses: $/.github/workflows/nextest.yml
with:
runs-on: windows-latest
shards: 2
unsharded:
name: unsharded nextest on linux
uses: $/.github/workflows/nextest.yml
with:
runs-on: ubuntu-latest
shards: 1

coverage:
name: check test coverage
needs: [linux, macos, windows, unsharded]
runs-on: ubuntu-slim
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: junit-results-fixture-*
path: test-results
- run: python3 nextest/tests/check-coverage.py test-results
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Astral GitHub Actions

Shared GitHub Actions for Astral projects.

- [Sharded nextest](nextest/README.md)
- [Release smoke tests](release-smoke-test/README.md)
54 changes: 54 additions & 0 deletions nextest/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Sharded nextest

Use `.github/workflows/nextest.yml` to fan a numeric shard count out into
independent runner jobs. Every shard checks out the caller and invokes its
`.github/actions/nextest` composite action with `partition`, `partitions`, and
`save-cache` inputs. That repository-local action owns project-specific toolchain,
cache, feature, and test-fixture configuration.

```yaml
jobs:
test-windows:
name: cargo test on windows
uses: astral-sh/github-actions/.github/workflows/nextest.yml@<commit>
with:
runs-on: windows-latest
shards: 3
```

The workflow accepts 1–256 shards and disables matrix fail-fast so a failing
shard does not cancel the others. Its small planning job runs on `ubuntu-slim`.
Set the caller job's `name` to identify the suite; the workflow names its jobs
`plan shards` and `shard N of M`, including `shard 1 of 1` for unsharded runs.
The caller should gate the workflow as usual and grant only `contents: read`.
The checkout and repository-local action run with the caller's permissions.

The repository-local action can use `astral-sh/github-actions/nextest@<commit>`
after preparing its environment:

```yaml
- uses: astral-sh/github-actions/nextest@<commit>
with:
name: windows
partition: ${{ inputs.partition }}
partitions: ${{ inputs.partitions }}
arguments: '["--workspace", "--profile", "ci"]'
```

The action installs nextest, passes the JSON argument array directly to Cargo,
and adds `--partition hash:N/T --no-tests=pass` when `T > 1`. Empty hash
partitions succeed; one shard runs the unpartitioned suite with nextest's normal
empty-suite behavior. It sets `INSTA_UPDATE=new` and `INSTA_PENDING_DIR`, uploads pending
snapshots after a failure, and uploads JUnit results even when tests fail.
Artifact names are `pending-snapshots-NAME[-N]` and `junit-results-NAME[-N]`;
the numeric suffix is omitted for one shard. Each independent suite in a
workflow run needs a distinct `name`.

`working-directory` selects the Cargo workspace. `junit-path` is relative to that
directory; `snapshots-path` is relative to the original checkout. Both accept
absolute paths. Artifacts default to 14-day retention. The nextest profile must
enable JUnit output if a JUnit artifact is required.

When moving jobs into this workflow, make any job-name-dependent build-cache
keys explicit. GitHub's aggregate reusable-workflow result includes every shard,
but individual check names acquire another reusable-workflow level.
63 changes: 63 additions & 0 deletions nextest/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Run nextest shard
description: Run one nextest hash partition and collect its test artifacts.
inputs:
name:
description: Unique artifact-name component for this test suite.
required: true
partition:
description: One-based partition number.
default: "1"
partitions:
description: Total number of partitions.
default: "1"
arguments:
description: JSON array of arguments for cargo nextest run.
default: "[]"
working-directory:
description: Cargo workspace directory, relative to the checkout or absolute.
default: "."
junit-path:
description: JUnit glob relative to the Cargo workspace.
default: "target/nextest/**/junit.xml"
snapshots-path:
description: Pending insta snapshot directory relative to the checkout.
default: "pending-snapshots"
retention-days:
description: Number of days to retain test artifacts.
default: "14"
runs:
using: composite
steps:
- name: Install cargo nextest
uses: taiki-e/install-action@6c6fd71fe4fb72c3697d269963d0e15df8adedad # v2.85.10
with:
tool: cargo-nextest
- name: Cargo test
id: run
# A separate immutable helper pin supports older self-hosted runner parsers.
uses: astral-sh/github-actions/nextest/run@e6fe1b51adba9d0b2c847e503fce90e8fb765188
with:
name: ${{ inputs.name }}
partition: ${{ inputs.partition }}
partitions: ${{ inputs.partitions }}
arguments: ${{ inputs.arguments }}
working-directory: ${{ inputs.working-directory }}
junit-path: ${{ inputs.junit-path }}
snapshots-path: ${{ inputs.snapshots-path }}
- name: Upload pending snapshots
if: ${{ failure() && steps.run.outputs.snapshots-name != '' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ steps.run.outputs.snapshots-name }}
path: ${{ steps.run.outputs.snapshots-path }}
if-no-files-found: ignore
include-hidden-files: true
retention-days: ${{ inputs.retention-days }}
- name: Upload JUnit results
if: ${{ always() && steps.run.outputs.junit-name != '' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ steps.run.outputs.junit-name }}
path: ${{ steps.run.outputs.junit-path }}
if-no-files-found: ignore
retention-days: ${{ inputs.retention-days }}
12 changes: 12 additions & 0 deletions nextest/plan/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: Plan nextest shards
description: Convert a positive shard count into a GitHub Actions matrix.
inputs:
shards:
description: Number of shards, from 1 through 256.
required: true
outputs:
partitions:
description: JSON array of one-based partition numbers.
runs:
using: node24
main: index.mjs
14 changes: 14 additions & 0 deletions nextest/plan/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { appendFileSync } from "node:fs";
import { partitionCount } from "../run/options.mjs";

try {
const count = partitionCount(process.env.INPUT_SHARDS);
const partitions = Array.from({ length: count }, (_, index) => index + 1);
appendFileSync(
process.env.GITHUB_OUTPUT,
`partitions=${JSON.stringify(partitions)}\n`,
);
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
36 changes: 36 additions & 0 deletions nextest/run/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Invoke nextest
description: Validate shard inputs and invoke cargo nextest without a shell.
inputs:
name:
required: true
description: Artifact-name component.
partition:
default: "1"
description: One-based partition number.
partitions:
default: "1"
description: Total number of partitions.
arguments:
default: "[]"
description: JSON array of nextest arguments.
working-directory:
default: "."
description: Cargo workspace directory.
junit-path:
default: "target/nextest/**/junit.xml"
description: JUnit glob relative to the Cargo workspace.
snapshots-path:
default: "pending-snapshots"
description: Pending snapshot directory relative to the checkout.
outputs:
junit-name:
description: Unique JUnit artifact name.
junit-path:
description: Absolute JUnit artifact glob.
snapshots-name:
description: Unique pending-snapshot artifact name.
snapshots-path:
description: Absolute pending-snapshot directory.
runs:
using: node24
main: index.mjs
37 changes: 37 additions & 0 deletions nextest/run/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { spawnSync } from "node:child_process";
import { appendFileSync } from "node:fs";
import { options } from "./options.mjs";

try {
const configuration = options(
{
name: process.env.INPUT_NAME,
partition: process.env.INPUT_PARTITION,
partitions: process.env.INPUT_PARTITIONS,
arguments: process.env.INPUT_ARGUMENTS,
directory: process.env["INPUT_WORKING-DIRECTORY"],
junit: process.env["INPUT_JUNIT-PATH"],
snapshots: process.env["INPUT_SNAPSHOTS-PATH"],
},
process.env.GITHUB_WORKSPACE,
);
for (const [name, value] of Object.entries(configuration.outputs)) {
if (/[\r\n]/.test(value))
throw new Error("Artifact paths cannot contain newlines");
appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`);
}
const result = spawnSync("cargo", configuration.args, {
cwd: configuration.directory,
stdio: "inherit",
env: {
...process.env,
INSTA_UPDATE: "new",
INSTA_PENDING_DIR: configuration.outputs["snapshots-path"],
},
});
if (result.error) throw result.error;
process.exitCode = result.status ?? 1;
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
Loading