Skip to content

Add release automation#266

Open
Eli Pinkerton (wallstop) wants to merge 25 commits into
mainfrom
dev/wallstop/resease-fixes-2
Open

Add release automation#266
Eli Pinkerton (wallstop) wants to merge 25 commits into
mainfrom
dev/wallstop/resease-fixes-2

Conversation

@wallstop

@wallstop Eli Pinkerton (wallstop) commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add manual Release Prepare workflow for major/minor/patch or explicit semver release PRs.
  • Add merge-to-tag and tag-to-publish workflows for GitHub Releases, npm publish, checksums, and .unitypackage export.
  • Add release helper scripts/tests plus npm package guards so dev/test artifacts cannot enter release payloads.

Validation

  • npm run validate:prepush
  • npm run validate:npm-package
  • bash scripts/unity/export-unitypackage.sh --stage-only --project-dir .artifacts/unity/unitypackage-stage-smoke
  • actionlint .github/workflows/release-prepare.yml .github/workflows/release-tag.yml .github/workflows/release.yml
  • adversarial sub-agent review x2: first found issues, second found no actionable findings after fixes

Note

High Risk
Automates tagging, npm publish, and GitHub Release asset upload with production secrets and Unity license usage; mistakes or credential gaps could ship wrong versions or block releases.

Overview
Replaces the maintainer release-drafter flow in contributing with a three-stage pipeline: manual Release Prepare (semver bump or explicit version, optional dry-run), merge-triggered Release Tag, and tag-triggered Release Publish.

Release Prepare runs prepare-release.ps1, syncs banner/issue-template versions, runs release and npm validation, then opens a release/X.Y.Z PR via a GitHub App token (with collision checks on tags/branches and release notes validated before push).

Release Tag tags default-branch commits whose message matches release: X.Y.Z (or squash-merge variant) when CHANGELOG.md has a fence-aware ## [version] heading; non-release package.json/CHANGELOG.md pushes no-op cleanly.

Release Publish verifies strict X.Y.Z tags against package.json and changelog, packs npm (checksums + notes), exports .unitypackage via new export-unitypackage.sh (npm pack → staged Unity project → Docker export under org Unity lock), then publishes npm (skip if exists) and a GitHub Release with tarball, .unitypackage, and .sha256 assets.

Supporting changes tighten the shipped artifact: explicit package.json files allowlist, expanded .npmignore, and a stricter validate-npm-package.ps1 (full tracked payload parity, case-sensitive paths, forbidden dev roots). Adds scripts/release-tools/*, test:release-tools, and workflow contract tests. unity-tests-single-threaded now waits for the main and standalone Unity jobs to reduce org-lock contention within the same workflow.

Reviewed by Cursor Bugbot for commit 8b2a360. Bugbot is set up for automated code reviews on this repo. Configure here.

Copilot AI review requested due to automatic review settings June 30, 2026 15:54
Comment thread .github/workflows/release-prepare.yml Outdated
Comment thread scripts/release-tools/release-helpers.ps1 Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new release automation pipeline that prepares a semver release PR, tags the merged release commit, and publishes GitHub Release assets + an npm package + a .unitypackage, with additional guardrails to prevent dev/CI artifacts from entering release payloads.

Changes:

  • Added GitHub Actions workflows for Release Prepare (manual), Release Tag (merge-triggered), and Release Publish (tag-triggered).
  • Added PowerShell release helper tooling + tests for semver bumping, changelog rotation, and release-notes generation.
  • Hardened npm packaging by adding a files whitelist and expanding npm package validation/ignore rules.

Reviewed changes

Copilot reviewed 13 out of 19 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
scripts/validate-npm-package.ps1 Adds stricter allow/deny-list validation of packed npm contents (top-level entries, scripts, and C# file roots).
scripts/unity/export-unitypackage.sh.meta Unity .meta for the new export script.
scripts/unity/export-unitypackage.sh Adds a Docker-Unity based .unitypackage export pipeline that stages from npm pack.
scripts/tests/test-release-tools.ps1.meta Unity .meta for the new test script.
scripts/tests/test-release-tools.ps1 Adds a lightweight PowerShell test runner for the release helper scripts.
scripts/release-tools/write-release-notes.ps1.meta Unity .meta for the new release-notes writer.
scripts/release-tools/write-release-notes.ps1 Adds a CLI wrapper that writes release notes derived from CHANGELOG.md.
scripts/release-tools/release-helpers.ps1.meta Unity .meta for the new helper module.
scripts/release-tools/release-helpers.ps1 Implements semver parsing/comparison, changelog rotation, and fence-aware changelog section extraction.
scripts/release-tools/prepare-release.ps1.meta Unity .meta for the new prepare script.
scripts/release-tools/prepare-release.ps1 Adds a CLI entrypoint to bump version + rotate changelog (supports dry-run).
scripts/release-tools.meta Adds Unity folder .meta for the new scripts/release-tools directory.
package.json Adds npm files whitelist and wires test:release-tools into validation.
docs/project/contributing.md Updates contributor docs to describe the new release process (replacing release-drafter).
.npmignore Expands ignore rules to exclude more dev/CI/editor artifacts from npm publishes.
.markdownlintignore Ignores PLAN.md and its .meta file for markdown linting.
.github/workflows/release.yml Adds tag-triggered Release Publish pipeline (validate/pack/export/publish).
.github/workflows/release-tag.yml Adds merge-triggered tagging workflow guarded by commit subject + changelog.
.github/workflows/release-prepare.yml Adds manual workflow to prepare and open a release PR (with dry-run option).
Files not reviewed (6)
  • scripts/release-tools.meta: Generated file
  • scripts/release-tools/prepare-release.ps1.meta: Generated file
  • scripts/release-tools/release-helpers.ps1.meta: Generated file
  • scripts/release-tools/write-release-notes.ps1.meta: Generated file
  • scripts/tests/test-release-tools.ps1.meta: Generated file
  • scripts/unity/export-unitypackage.sh.meta: Generated file

Comment on lines +38 to +43
ARTIFACTS_ROOT="$(realpath -m "${REPO_ROOT}/.artifacts")"
PROJECT_DIR="$(realpath -m "${PROJECT_DIR}")"
if [[ "${PROJECT_DIR}" != "${ARTIFACTS_ROOT}" && "${PROJECT_DIR}" != "${ARTIFACTS_ROOT}/"* ]]; then
echo "ERROR: Refusing to create the export project outside ${ARTIFACTS_ROOT}: ${PROJECT_DIR}" >&2
exit 1
fi
Comment thread .github/workflows/release.yml Outdated
Comment on lines +51 to +55
escaped_version="${package_version//./\\.}"
if ! grep -Eq "^## \[${escaped_version}\]( - [0-9]{4}-[0-9]{2}-[0-9]{2})?$" CHANGELOG.md; then
echo "::error::CHANGELOG.md has no exact heading for ${package_version}."
exit 1
fi
Comment thread .github/workflows/release-tag.yml Outdated
Comment on lines +66 to +69
if ! grep -Eq "^## \[${escaped_version}\]( - [0-9]{4}-[0-9]{2}-[0-9]{2})?$" CHANGELOG.md; then
echo "::error::Release commit for ${version} has no matching CHANGELOG.md heading."
exit 1
fi

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 19 changed files in this pull request and generated 2 comments.

Files not reviewed (6)
  • scripts/release-tools.meta: Generated file
  • scripts/release-tools/prepare-release.ps1.meta: Generated file
  • scripts/release-tools/release-helpers.ps1.meta: Generated file
  • scripts/release-tools/write-release-notes.ps1.meta: Generated file
  • scripts/tests/test-release-tools.ps1.meta: Generated file
  • scripts/unity/export-unitypackage.sh.meta: Generated file

Comment thread .github/workflows/release.yml
Comment thread .github/workflows/release.yml
Comment thread .github/workflows/release-tag.yml Outdated
Comment thread .github/workflows/release-tag.yml

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 21 changed files in this pull request and generated 2 comments.

Files not reviewed (6)
  • scripts/release-tools.meta: Generated file
  • scripts/release-tools/prepare-release.ps1.meta: Generated file
  • scripts/release-tools/release-helpers.ps1.meta: Generated file
  • scripts/release-tools/write-release-notes.ps1.meta: Generated file
  • scripts/tests/test-release-tools.ps1.meta: Generated file
  • scripts/unity/export-unitypackage.sh.meta: Generated file

Comment thread scripts/unity/export-unitypackage.sh Outdated
throw new InvalidOperationException("Missing -exportOutput argument.");
}

Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
throw "Expected exactly one package.json version property for '$CurrentVersion'; found $($matches.Count)."
}

$updated = [regex]::Replace($Content, $pattern, "`${1}$NextVersion`${2}", 1)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 21 changed files in this pull request and generated 2 comments.

Files not reviewed (6)
  • scripts/release-tools.meta: Generated file
  • scripts/release-tools/prepare-release.ps1.meta: Generated file
  • scripts/release-tools/release-helpers.ps1.meta: Generated file
  • scripts/release-tools/write-release-notes.ps1.meta: Generated file
  • scripts/tests/test-release-tools.ps1.meta: Generated file
  • scripts/unity/export-unitypackage.sh.meta: Generated file

Comment thread scripts/unity/export-unitypackage.sh Outdated
throw new InvalidOperationException("Missing -exportOutput argument.");
}

Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
Comment on lines +88 to +110
$inFence = $false
$fenceMarker = ''

for ($index = 0; $index -lt $Lines.Count; $index++) {
$line = $Lines[$index]
$trimmed = $line.TrimStart()
$isFenceLine = $false

if ($trimmed -match '^(?<marker>`{3,}|~{3,})') {
$marker = $Matches['marker']
$markerPrefix = $marker.Substring(0, 1)
if (-not $inFence) {
$inFence = $true
$fenceMarker = $markerPrefix
$isFenceLine = $true
} elseif ($fenceMarker -eq $markerPrefix) {
$isFenceLine = $true
$mask[$index] = $true
$inFence = $false
$fenceMarker = ''
continue
}
}

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 21 changed files in this pull request and generated 4 comments.

Files not reviewed (6)
  • scripts/release-tools.meta: Generated file
  • scripts/release-tools/prepare-release.ps1.meta: Generated file
  • scripts/release-tools/release-helpers.ps1.meta: Generated file
  • scripts/release-tools/write-release-notes.ps1.meta: Generated file
  • scripts/tests/test-release-tools.ps1.meta: Generated file
  • scripts/unity/export-unitypackage.sh.meta: Generated file

Comment thread package.json
Comment on lines +99 to +115
"files": [
"CHANGELOG.md",
"CHANGELOG.md.meta",
"Editor",
"Editor.meta",
"LICENSE",
"LICENSE.meta",
"README.md",
"README.md.meta",
"Runtime",
"Runtime.meta",
"Samples~",
"docs",
"docs.meta",
"package.json.meta",
"scripts/postinstall-hooks.js"
],
Comment on lines +72 to +89
$allowedTopLevelEntries = @(
'CHANGELOG.md',
'CHANGELOG.md.meta',
'Editor',
'Editor.meta',
'LICENSE',
'LICENSE.meta',
'README.md',
'README.md.meta',
'Runtime',
'Runtime.meta',
'Samples~',
'docs',
'docs.meta',
'package.json',
'package.json.meta',
'scripts'
)
Comment thread scripts/validate-npm-package.ps1 Outdated
}
}

$allowedCsRoots = @('Runtime/', 'Editor/', 'Samples~/')
Comment on lines +121 to +136
for entry in \
package.json \
package.json.meta \
README.md \
README.md.meta \
LICENSE \
LICENSE.meta \
CHANGELOG.md \
CHANGELOG.md.meta \
Runtime \
Runtime.meta \
Editor \
Editor.meta
do
copy_package_entry "${entry}" required
done
Comment thread .github/workflows/release-prepare.yml
Comment thread .github/workflows/release-tag.yml Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 21 changed files in this pull request and generated 1 comment.

Files not reviewed (6)
  • scripts/release-tools.meta: Generated file
  • scripts/release-tools/prepare-release.ps1.meta: Generated file
  • scripts/release-tools/release-helpers.ps1.meta: Generated file
  • scripts/release-tools/write-release-notes.ps1.meta: Generated file
  • scripts/tests/test-release-tools.ps1.meta: Generated file
  • scripts/unity/export-unitypackage.sh.meta: Generated file

Comment on lines +105 to +116
$scriptsDir = Join-Path $packageDir 'scripts'
if (Test-Path -LiteralPath $scriptsDir) {
$allowedScriptsEntries = @('postinstall-hooks.js')
$scriptEntries = Get-ChildItem -LiteralPath $scriptsDir -Recurse -File | ForEach-Object {
$_.FullName.Replace("$scriptsDir\", "").Replace("$scriptsDir/", "") -replace '\\', '/'
}
foreach ($entry in $scriptEntries) {
if ($entry -notin $allowedScriptsEntries) {
$errors += "Unexpected script included in npm package: scripts/$entry"
}
}
}
Comment thread scripts/release-tools/release-helpers.ps1
Comment thread .github/workflows/release-prepare.yml

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 24 changed files in this pull request and generated 3 comments.

Files not reviewed (6)
  • scripts/release-tools.meta: Generated file
  • scripts/release-tools/prepare-release.ps1.meta: Generated file
  • scripts/release-tools/release-helpers.ps1.meta: Generated file
  • scripts/release-tools/write-release-notes.ps1.meta: Generated file
  • scripts/tests/test-release-tools.ps1.meta: Generated file
  • scripts/unity/export-unitypackage.sh.meta: Generated file

Comment thread .github/workflows/release.yml Outdated
# enforces exact X.Y.Z; these exclusions avoid noisy prerelease/build runs.
- "[0-9]*.[0-9]*.[0-9]*"
- "![0-9]*.[0-9]*.[0-9]*-*"
- '![0-9]*.[0-9]*.[0-9]*\+*'
$publishTriggerExcludesPrereleaseTags = (
$publishWorkflowContent.Contains('- "[0-9]*.[0-9]*.[0-9]*"') -and
$publishWorkflowContent.Contains('- "![0-9]*.[0-9]*.[0-9]*-*"') -and
$publishWorkflowContent.Contains("- '![0-9]*.[0-9]*.[0-9]*\+*'") -and
}
}

# Get all files and subdirectories in this folder (recursively)
Comment thread .github/workflows/release.yml Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 24 changed files in this pull request and generated 2 comments.

Files not reviewed (6)
  • scripts/release-tools.meta: Generated file
  • scripts/release-tools/prepare-release.ps1.meta: Generated file
  • scripts/release-tools/release-helpers.ps1.meta: Generated file
  • scripts/release-tools/write-release-notes.ps1.meta: Generated file
  • scripts/tests/test-release-tools.ps1.meta: Generated file
  • scripts/unity/export-unitypackage.sh.meta: Generated file

Comment thread .github/workflows/release.yml Outdated
Comment on lines +6 to +8
# GitHub tag filters are globs, not regex. The verify job below still
# enforces no-leading-zero X.Y.Z semver.
- "[0-9]+.[0-9]+.[0-9]+"
Comment on lines +969 to +973
$publishTriggerNarrowlyMatchesReleaseTags = (
$publishWorkflowContent.Contains('- "[0-9]+.[0-9]+.[0-9]+"') -and
-not $publishWorkflowContent.Contains('- "[0-9]*.[0-9]*.[0-9]*"') -and
$publishWorkflowContent.Contains('Release tags must use unprefixed X.Y.Z semver.')
)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 24 changed files in this pull request and generated 1 comment.

Files not reviewed (6)
  • scripts/release-tools.meta: Generated file
  • scripts/release-tools/prepare-release.ps1.meta: Generated file
  • scripts/release-tools/release-helpers.ps1.meta: Generated file
  • scripts/release-tools/write-release-notes.ps1.meta: Generated file
  • scripts/tests/test-release-tools.ps1.meta: Generated file
  • scripts/unity/export-unitypackage.sh.meta: Generated file

Comment thread scripts/unity/export-unitypackage.sh Outdated
Comment on lines +32 to +36
PACKAGE_NAME="$(jq -r '.name' "${REPO_ROOT}/package.json")"
PACKAGE_VERSION="$(jq -r '.version' "${REPO_ROOT}/package.json")"
if [[ -z "${OUTPUT_PATH}" ]]; then
OUTPUT_PATH="${REPO_ROOT}/.artifacts/release/${PACKAGE_NAME}-${PACKAGE_VERSION}.unitypackage"
fi

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 24 changed files in this pull request and generated 2 comments.

Files not reviewed (6)
  • scripts/release-tools.meta: Generated file
  • scripts/release-tools/prepare-release.ps1.meta: Generated file
  • scripts/release-tools/release-helpers.ps1.meta: Generated file
  • scripts/release-tools/write-release-notes.ps1.meta: Generated file
  • scripts/tests/test-release-tools.ps1.meta: Generated file
  • scripts/unity/export-unitypackage.sh.meta: Generated file

Comment thread .github/workflows/release.yml Outdated
Comment on lines +4 to +9
push:
tags:
# GitHub tag filters are globs, not regex; their filter-pattern cheat
# sheet defines + as one-or-more of the preceding character. The verify
# job below still enforces no-leading-zero X.Y.Z semver.
- "[0-9]+.[0-9]+.[0-9]+"
Comment on lines +969 to +973
$publishTriggerNarrowlyMatchesReleaseTags = (
$publishWorkflowContent.Contains('- "[0-9]+.[0-9]+.[0-9]+"') -and
-not $publishWorkflowContent.Contains('- "[0-9]*.[0-9]*.[0-9]*"') -and
$publishWorkflowContent.Contains('Release tags must use unprefixed X.Y.Z semver.')
)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 24 changed files in this pull request and generated 1 comment.

Files not reviewed (6)
  • scripts/release-tools.meta: Generated file
  • scripts/release-tools/prepare-release.ps1.meta: Generated file
  • scripts/release-tools/release-helpers.ps1.meta: Generated file
  • scripts/release-tools/write-release-notes.ps1.meta: Generated file
  • scripts/tests/test-release-tools.ps1.meta: Generated file
  • scripts/unity/export-unitypackage.sh.meta: Generated file

Comment thread scripts/validate-npm-package.ps1 Outdated
'postinstall-hooks.js',
'postinstall-hooks.js.meta'
)
$scriptEntries = Get-ChildItem -LiteralPath $scriptsDir -Recurse -File | ForEach-Object {

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 24 changed files in this pull request and generated 3 comments.

Files not reviewed (6)
  • scripts/release-tools.meta: Generated file
  • scripts/release-tools/prepare-release.ps1.meta: Generated file
  • scripts/release-tools/release-helpers.ps1.meta: Generated file
  • scripts/release-tools/write-release-notes.ps1.meta: Generated file
  • scripts/tests/test-release-tools.ps1.meta: Generated file
  • scripts/unity/export-unitypackage.sh.meta: Generated file
Comments suppressed due to low confidence (1)

scripts/validate-npm-package.ps1:300

  • Folder meta validation builds $relativePath via string Replace on the absolute path. This can produce incorrect relative paths (and therefore confusing errors) if the extracted path format differs. GetRelativePath is safer and consistent with git-style paths.
      # Get relative path for better error messages
      $relativePath = $item.FullName.Replace("$packageDir\", "").Replace("$packageDir/", "")
      $relativePath = $relativePath -replace '\\', '/'

Comment on lines +49 to +53
Get-ChildItem -LiteralPath $PackageDir -Recurse -File -Force |
ForEach-Object {
$_.FullName.Replace("$PackageDir\", "").Replace("$PackageDir/", "") -replace '\\', '/'
} |
Sort-Object
Comment on lines +183 to +185
$scriptEntries = Get-ChildItem -LiteralPath $scriptsDir -Recurse -File -Force | ForEach-Object {
$_.FullName.Replace("$scriptsDir\", "").Replace("$scriptsDir/", "") -replace '\\', '/'
}
Comment on lines +255 to +258
$allowedCsRoots = @('Runtime/', 'Editor/', 'Samples~/', 'Styles/')
$packedCsFiles = Get-ChildItem -LiteralPath $packageDir -Recurse -File -Filter '*.cs' -Force | ForEach-Object {
$_.FullName.Replace("$packageDir\", "").Replace("$packageDir/", "") -replace '\\', '/'
}

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a25d75b. Configure here.

}
'; then
echo "::error::Release commit for ${version} has no matching CHANGELOG.md heading."
exit 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tag skips changelog content check

Medium Severity

The Release Tag job only uses Test-ChangelogVersionHeading before pushing the semver tag. Publish later builds release notes with Get-ChangelogSection, which rejects a version section that has a heading but no actual release-note lines. A release-titled merge can therefore get a tag while Release Publish fails after the tag already exists.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a25d75b. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 26 changed files in this pull request and generated no new comments.

Files not reviewed (6)
  • scripts/release-tools.meta: Generated file
  • scripts/release-tools/prepare-release.ps1.meta: Generated file
  • scripts/release-tools/release-helpers.ps1.meta: Generated file
  • scripts/release-tools/write-release-notes.ps1.meta: Generated file
  • scripts/tests/test-release-tools.ps1.meta: Generated file
  • scripts/unity/export-unitypackage.sh.meta: Generated file

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants