diff --git a/.github/workflows/development-buildandtestupmrelease.yml b/.github/workflows/development-buildandtestupmrelease.yml index 570fcd5..9e79151 100644 --- a/.github/workflows/development-buildandtestupmrelease.yml +++ b/.github/workflows/development-buildandtestupmrelease.yml @@ -23,7 +23,8 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + # os: [ubuntu-latest, windows-latest, macos-latest] + os: [windows-latest, macos-latest] unity-version: - '6000.0.x' - '6000.1' diff --git a/.github/workflows/getpackageversionfrompackage.yml b/.github/workflows/getpackageversionfrompackage.yml new file mode 100644 index 0000000..42fb5e1 --- /dev/null +++ b/.github/workflows/getpackageversionfrompackage.yml @@ -0,0 +1,51 @@ +name: Get the package version from a UPM package.json file + +on: + workflow_call: + inputs: + build-host: + required: true + type: string + target-branch: + description: Branch to read package.json from. Defaults to the triggering ref. + required: false + type: string + default: ${{ github.ref }} + version-file-path: + description: Optional path to the package.json to read. Defaults to package.json in the repo root. + required: false + type: string + default: package.json + outputs: + packageversion: + description: The version field of the UPM package + value: ${{ jobs.get_package_version.outputs.upmpackageversion }} + +jobs: + get_package_version: + name: Get package version from UPM package + runs-on: ${{ inputs.build-host }} + outputs: + upmpackageversion: ${{ steps.getVersion.outputs.packageversion }} + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.target-branch }} + + - id: getVersion + name: Read package version + shell: bash + env: + VERSION_FILE: ${{ inputs.version-file-path }} + run: | + if [ ! -f "$VERSION_FILE" ]; then + echo "::error::No package.json found at $VERSION_FILE" + exit 1 + fi + version=$(jq -r '.version // empty' "$VERSION_FILE") + if [ -z "$version" ]; then + echo "::error::package.json at $VERSION_FILE has no version" + exit 1 + fi + echo "Detected package version $version" + echo "packageversion=$version" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/main-publish.yml b/.github/workflows/main-publish.yml new file mode 100644 index 0000000..ad8ef57 --- /dev/null +++ b/.github/workflows/main-publish.yml @@ -0,0 +1,115 @@ +name: Publish main branch and increment version + +# Runs when a pull request into main is merged. The PR title selects the release type: +# - contains "no-ver" - tag the version already in package.json, no bump +# - contains "major-release" - bump major (1.x.x -> 2.0.0) +# - contains "minor-release" - bump minor (1.0.x -> 1.1.0) +# - anything else - patch release: strip the pre-release suffix (1.0.0-pre.1 -> 1.0.0), +# or bump patch if there is no suffix (1.0.0 -> 1.0.1) +# +# After tagging, development is refreshed from main and moved to the next pre-release (1.0.1-pre.1). +# +# All commits and tags are pushed by the ui-extensions-bot GitHub App using the org secrets +# RELEASE_APP_CLIENT_ID and RELEASE_APP_PRIVATE_KEY. No personal access token is involved. + +on: + pull_request: + types: + - closed + branches: + - main + +permissions: + contents: read + +concurrency: + group: release-${{ github.event.pull_request.base.ref }} + cancel-in-progress: false + +jobs: + # Read the version to tag when the PR title contains "no-ver" (no version bump) + validate-environment: + if: github.event.pull_request.merged == true && contains(github.event.pull_request.title, 'no-ver') + name: Get version from UPM package + uses: ./.github/workflows/getpackageversionfrompackage.yml + with: + build-host: ubuntu-latest + target-branch: ${{ github.event.pull_request.base.ref }} + + release-package-only: + needs: validate-environment + name: Release package only, no upversion + uses: ./.github/workflows/tagrelease.yml + with: + build-host: ubuntu-latest + target-branch: ${{ github.event.pull_request.base.ref }} + version: ${{ needs.validate-environment.outputs.packageversion }} + secrets: inherit + + upversion-major-package: + if: github.event.pull_request.merged == true && contains(github.event.pull_request.title, 'no-ver') == false && contains(github.event.pull_request.title, 'major-release') + name: Major version package and release + uses: ./.github/workflows/upversionandtagrelease.yml + with: + build-host: ubuntu-latest + build-type: major + target-branch: ${{ github.event.pull_request.base.ref }} + secrets: inherit + + upversion-minor-package: + if: github.event.pull_request.merged == true && contains(github.event.pull_request.title, 'no-ver') == false && contains(github.event.pull_request.title, 'minor-release') + name: Minor version package and release + uses: ./.github/workflows/upversionandtagrelease.yml + with: + build-host: ubuntu-latest + build-type: minor + target-branch: ${{ github.event.pull_request.base.ref }} + secrets: inherit + + # Default path when no release keyword is in the PR title + upversion-patch-package: + if: github.event.pull_request.merged == true && contains(github.event.pull_request.title, 'no-ver') == false && contains(github.event.pull_request.title, 'minor-release') == false && contains(github.event.pull_request.title, 'major-release') == false + name: Patch version package and release + uses: ./.github/workflows/upversionandtagrelease.yml + with: + build-host: ubuntu-latest + build-type: patch-release + target-branch: ${{ github.event.pull_request.base.ref }} + secrets: inherit + + release-complete: + # Runs only for a merged PR and only if no release job failed. The release jobs that did not + # match the PR title are skipped, which is fine. A real failure skips this job so the + # development refresh never runs from a half-finished release. + if: ${{ github.event.pull_request.merged == true && !failure() && !cancelled() }} + needs: [upversion-major-package, upversion-minor-package, upversion-patch-package, release-package-only] + name: Release complete + runs-on: ubuntu-latest + steps: + - name: Release done + run: echo "Release done, refreshing development" + + # Merge the released main branch back into development + refresh-development: + if: ${{ needs.release-complete.result == 'success' }} + needs: [release-complete] + name: Refresh development branch + uses: ./.github/workflows/refreshbranch.yml + with: + build-host: ubuntu-latest + target-branch: development + source-branch: ${{ github.event.pull_request.base.ref }} + secrets: inherit + + # Move development to the next pre-release version, no tag + upversion-development: + if: ${{ needs.refresh-development.result == 'success' }} + needs: [refresh-development] + name: Upversion the development branch for the next release + uses: ./.github/workflows/upversionandtagrelease.yml + with: + build-host: ubuntu-latest + build-type: patch-pre + target-branch: development + createTag: false + secrets: inherit diff --git a/.github/workflows/refreshbranch.yml b/.github/workflows/refreshbranch.yml new file mode 100644 index 0000000..2a9bc27 --- /dev/null +++ b/.github/workflows/refreshbranch.yml @@ -0,0 +1,71 @@ +name: Refresh branch + +# Merges one branch into another and pushes the result. Used after a release to bring +# the version bump on main back into development. + +on: + workflow_call: + inputs: + build-host: + required: true + type: string + target-branch: + description: Branch that receives the merge, for example development + required: true + type: string + source-branch: + description: Branch merged into the target, for example main + required: true + type: string + secrets: + RELEASE_APP_CLIENT_ID: + required: true + RELEASE_APP_PRIVATE_KEY: + required: true + +jobs: + refreshBranch: + name: Refresh ${{ inputs.target-branch }} from ${{ inputs.source-branch }} + runs-on: ${{ inputs.build-host }} + steps: + - name: Create release App token + uses: actions/create-github-app-token@v3 + id: app-token + with: + client-id: ${{ secrets.RELEASE_APP_CLIENT_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + + - name: Get App bot user id + id: bot + shell: bash + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} + run: echo "id=$(gh api "/users/${APP_SLUG}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" + + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.target-branch }} + fetch-depth: 0 + clean: true + token: ${{ steps.app-token.outputs.token }} + + - name: Configure git identity + shell: bash + env: + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} + BOT_ID: ${{ steps.bot.outputs.id }} + run: | + git config --global user.name "${APP_SLUG}[bot]" + git config --global user.email "${BOT_ID}+${APP_SLUG}[bot]@users.noreply.github.com" + + - name: Merge source branch into target branch + shell: bash + env: + TARGET_BRANCH: ${{ inputs.target-branch }} + SOURCE_BRANCH: ${{ inputs.source-branch }} + run: | + git fetch origin "$SOURCE_BRANCH" + git merge --no-edit -m "Refresh $TARGET_BRANCH from $SOURCE_BRANCH [skip ci]" FETCH_HEAD + git push origin "HEAD:$TARGET_BRANCH" + echo "Branch $TARGET_BRANCH updated with changes from $SOURCE_BRANCH" diff --git a/.github/workflows/release-preflight.yml b/.github/workflows/release-preflight.yml new file mode 100644 index 0000000..41e3638 --- /dev/null +++ b/.github/workflows/release-preflight.yml @@ -0,0 +1,74 @@ +name: Release preflight + +# Runs on pull requests into main (the release PRs) and on demand. It publishes nothing. +# It confirms the ui-extensions-bot GitHub App can mint a token with push access to this repo, +# and reports the version main-publish will produce when the PR is merged. + +on: + pull_request: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate-release-app: + name: Validate release App and version + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Create release App token + uses: actions/create-github-app-token@v3 + id: app-token + with: + client-id: ${{ secrets.RELEASE_APP_CLIENT_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + + - name: Check the App can push to this repo + shell: bash + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} + run: | + push=$(gh api "repos/$GITHUB_REPOSITORY" --jq '.permissions.push') + if [ "$push" != "true" ]; then + echo "::error::App $APP_SLUG does not have push access to $GITHUB_REPOSITORY. Check the App installation and its Contents permission." + exit 1 + fi + botid=$(gh api "/users/${APP_SLUG}[bot]" --jq .id) + echo "App $APP_SLUG (bot user id $botid) can push to $GITHUB_REPOSITORY" + + - name: Report the version this PR will release + shell: bash + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + version=$(jq -r .version package.json) + base="${version%%-*}" + IFS=. read -r major minor patch <<< "$base" + case "$PR_TITLE" in + *no-ver*) kind="no bump"; next="$version" ;; + *major-release*) kind="major bump"; next="$((major + 1)).0.0" ;; + *minor-release*) kind="minor bump"; next="$major.$((minor + 1)).0" ;; + *) + kind="patch release" + if [ "$base" != "$version" ]; then next="$base"; else next="$major.$minor.$((patch + 1))"; fi + ;; + esac + echo "package.json version: $version" + echo "PR title: $PR_TITLE" + echo "On merge, main-publish will do a $kind and tag v$next" + if git ls-remote --exit-code --tags origin "refs/tags/v$next" > /dev/null 2>&1; then + echo "::error::Tag v$next already exists. The release would fail at the tag step." + exit 1 + fi + { + echo "## Release preflight" + echo "" + echo "- package.json version: \`$version\`" + echo "- release type: $kind" + echo "- tag on merge: \`v$next\`" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/tagrelease.yml b/.github/workflows/tagrelease.yml new file mode 100644 index 0000000..0fa395e --- /dev/null +++ b/.github/workflows/tagrelease.yml @@ -0,0 +1,85 @@ +name: Tag Release + +# Tags the head of a branch with v without changing package.json. +# Called by main-publish for PRs whose title contains "no-ver". + +on: + workflow_call: + inputs: + build-host: + required: true + type: string + version: + description: Version to tag, without the leading v. Must match package.json. + required: true + type: string + target-branch: + description: Branch whose head is tagged. Defaults to the triggering ref. + required: false + type: string + default: ${{ github.ref }} + secrets: + RELEASE_APP_CLIENT_ID: + required: true + RELEASE_APP_PRIVATE_KEY: + required: true + +jobs: + packageRelease: + name: Tag UPM package release + runs-on: ${{ inputs.build-host }} + steps: + - name: Create release App token + uses: actions/create-github-app-token@v3 + id: app-token + with: + client-id: ${{ secrets.RELEASE_APP_CLIENT_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + + - name: Get App bot user id + id: bot + shell: bash + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} + run: echo "id=$(gh api "/users/${APP_SLUG}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" + + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.target-branch }} + fetch-depth: 0 + clean: true + token: ${{ steps.app-token.outputs.token }} + + - name: Configure git identity + shell: bash + env: + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} + BOT_ID: ${{ steps.bot.outputs.id }} + run: | + git config --global user.name "${APP_SLUG}[bot]" + git config --global user.email "${BOT_ID}+${APP_SLUG}[bot]@users.noreply.github.com" + + - name: Check if tag exists + shell: pwsh + env: + TAG_NAME: v${{ inputs.version }} + run: | + if (git tag --list $env:TAG_NAME) { + Write-Error "$env:TAG_NAME tag already exists" + exit 1 + } + Write-Host "$env:TAG_NAME is available" + + - name: Create tag and push + shell: pwsh + env: + TAG_NAME: v${{ inputs.version }} + run: | + git tag -a $env:TAG_NAME -m "$env:TAG_NAME Release [skip ci]" + git push origin $env:TAG_NAME + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to push tag $env:TAG_NAME" + exit 1 + } + Write-Host "Tagged $(git rev-parse --short HEAD) as $env:TAG_NAME" diff --git a/.github/workflows/upversionandtagrelease.yml b/.github/workflows/upversionandtagrelease.yml new file mode 100644 index 0000000..38f4d6f --- /dev/null +++ b/.github/workflows/upversionandtagrelease.yml @@ -0,0 +1,173 @@ +name: UpVersion UPM package and create release tag + +# Bumps the version in package.json on a branch, commits and pushes the bump, and optionally tags it. +# Called by main-publish for the release itself (on main) and for the follow-up bump on development. + +on: + workflow_call: + inputs: + build-host: + required: true + type: string + build-type: + description: | + How to change the version in package.json. One of: + build - 1.0.0-pre.1+1 increment build metadata + pre-release - 1.0.0-pre.2 increment the pre-release number + patch-release - 1.0.0 strip the pre-release suffix, or bump patch if there is none + patch - 1.0.1 bump patch + patch-pre - 1.0.1-pre.1 bump patch and start a new pre-release + minor - 1.1.0 bump minor + major - 2.0.0 bump major + required: false + default: pre-release + type: string + target-branch: + description: Branch name to bump and push. Must be a branch name such as main or development. + required: false + type: string + default: ${{ github.ref }} + createTag: + description: Tag the bump commit as v and push the tag. + required: false + type: boolean + default: true + outputs: + packageversion: + description: The new version written to package.json + value: ${{ jobs.packageRelease.outputs.packageversion }} + secrets: + RELEASE_APP_CLIENT_ID: + required: true + RELEASE_APP_PRIVATE_KEY: + required: true + +jobs: + packageRelease: + name: Bump UPM package version and tag + runs-on: ${{ inputs.build-host }} + outputs: + packageversion: ${{ steps.getpackageversion.outputs.packageversion }} + steps: + - name: Create release App token + uses: actions/create-github-app-token@v3 + id: app-token + with: + client-id: ${{ secrets.RELEASE_APP_CLIENT_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + + - name: Get App bot user id + id: bot + shell: bash + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} + run: echo "id=$(gh api "/users/${APP_SLUG}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" + + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.target-branch }} + fetch-depth: 0 + clean: true + token: ${{ steps.app-token.outputs.token }} + + - name: Configure git identity + shell: bash + env: + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} + BOT_ID: ${{ steps.bot.outputs.id }} + run: | + git config --global user.name "${APP_SLUG}[bot]" + git config --global user.email "${BOT_ID}+${APP_SLUG}[bot]@users.noreply.github.com" + + - id: getpackageversion + name: Bump UPM package version + shell: pwsh + env: + BUILD_TYPE: ${{ inputs.build-type }} + TARGET_BRANCH: ${{ inputs.target-branch }} + run: | + $packageFile = 'package.json' + if (-not (Test-Path $packageFile)) { + Write-Error "No package.json found at $packageFile" + exit 1 + } + + $content = Get-Content $packageFile -Raw + $packageInfo = $content | ConvertFrom-Json + $current = [System.Management.Automation.SemanticVersion]$packageInfo.version + Write-Host "Current package version: $current" + + $major = $current.Major + $minor = $current.Minor + $patch = $current.Patch + $preLabel = $current.PreReleaseLabel + $preNumber = if ($preLabel) { [int]($preLabel -replace '^pre\.', '') } else { 0 } + $build = if ($current.BuildLabel) { [int]$current.BuildLabel } else { 0 } + + $semver = [System.Management.Automation.SemanticVersion] + switch ($env:BUILD_TYPE) { + 'build' { $new = $semver::new($major, $minor, $patch, $preLabel, "$($build + 1)") } + 'pre-release' { $new = $semver::new($major, $minor, $patch, "pre.$($preNumber + 1)") } + 'patch-release' { + if ($preLabel) { $new = $semver::new($major, $minor, $patch) } + else { $new = $semver::new($major, $minor, $patch + 1) } + } + 'patch' { $new = $semver::new($major, $minor, $patch + 1) } + 'patch-pre' { $new = $semver::new($major, $minor, $patch + 1, 'pre.1') } + 'minor' { $new = $semver::new($major, $minor + 1, 0) } + 'major' { $new = $semver::new($major + 1, 0, 0) } + default { + Write-Error "Unknown build-type '$env:BUILD_TYPE'" + exit 1 + } + } + + $newVersion = $new.ToString() + Write-Host "Upgrading package version [$current] to [$newVersion]" + + # Replace only the top-level version field so the rest of package.json keeps its formatting + $pattern = [regex]'"version"\s*:\s*"[^"]*"' + $updated = $pattern.Replace($content, ('"version": "' + $newVersion + '"'), 1) + [System.IO.File]::WriteAllText((Resolve-Path $packageFile).Path, $updated) + + if (git status --porcelain -- $packageFile) { + git add $packageFile + git commit -m "Auto increment package version to $newVersion [skip ci]" + git push origin "HEAD:$env:TARGET_BRANCH" + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to push version bump to $env:TARGET_BRANCH" + exit 1 + } + } + else { + Write-Host "package.json already at $newVersion, nothing to commit" + } + + "packageversion=$newVersion" >> $env:GITHUB_OUTPUT + + - name: Check if tag exists + if: ${{ inputs.createTag == true }} + shell: pwsh + env: + TAG_NAME: v${{ steps.getpackageversion.outputs.packageversion }} + run: | + if (git tag --list $env:TAG_NAME) { + Write-Error "$env:TAG_NAME tag already exists" + exit 1 + } + Write-Host "$env:TAG_NAME is available" + + - name: Publish package tag + if: ${{ inputs.createTag == true }} + shell: pwsh + env: + TAG_NAME: v${{ steps.getpackageversion.outputs.packageversion }} + run: | + git tag -a $env:TAG_NAME -m "$env:TAG_NAME Release" + git push origin $env:TAG_NAME + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to push tag $env:TAG_NAME" + exit 1 + } + Write-Host "Tagged $(git rev-parse --short HEAD) as $env:TAG_NAME" diff --git a/README.md b/README.md index 5473742..cd49568 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,18 @@ -# Unity UI Toolkit Extensions README + +

+ + Unity UI Toolkit Extensions + +

-The UI Toolkit library of controls, components and other useful features for extending the UI Toolkit solution for Unity +# Unity UI Toolkit Extensions - [![Build and test UPM packages for platforms, all branches except main](https://github.com/Unity-UI-Extensions/com.unity.uitoolkitextensions/actions/workflows/development-buildandtestupmrelease.yml/badge.svg)](https://github.com/Unity-UI-Extensions/com.unity.uitoolkitextensions/actions/workflows/development-buildandtestupmrelease.yml) +The UI Toolkit library of controls, components and other useful features for extending the UI Toolkit solution for Unity. + ## Updates You can follow the UI Toolkit Extensions team for updates and news on: @@ -40,20 +46,22 @@ All funds go to support the project, no matter the amount. **Donations in code a ----- -## [Getting Started](#getting-started) +## [Getting Started](https://unity-ui-extensions.github.io/uitoolkit/) -Coming Soon. +1. Install the package, see [How do I get set up?](#how-do-i-get-set-up) below. +2. Open the Package Manager window, select **Unity UI Toolkit Extensions** and import the **UI Toolkit Extensions Samples** from the Samples tab to get the example scenes. +3. Browse the [UI Toolkit Extensions documentation](https://unity-ui-extensions.github.io/uitoolkit/) for control guides, API references and code examples. ----- ## [Controls and extensions listed in this project](https://unity-ui-extensions.github.io/uitoolkit/#controls) -The package ships **25 controls** across navigation, layout, forms, media, toggles and feedback, plus reusable utilities, and **12 ready-to-run sample scenes** — all using the `Unity.UI.Extensions` namespace. +The package ships **25 controls** across navigation, layout, forms, media, toggles and feedback, plus reusable utilities, and **12 ready-to-run sample scenes**. Everything lives in the `Unity.UI.Extensions` namespace. Browse the full, searchable list with API references and code examples: -- **[Controls →](https://unity-ui-extensions.github.io/uitoolkit/#controls)** -- **[Example scenes →](https://unity-ui-extensions.github.io/uitoolkit/examples/)** +- **[Controls](https://unity-ui-extensions.github.io/uitoolkit/#controls)** +- **[Example scenes](https://unity-ui-extensions.github.io/uitoolkit/examples/)** See the [CHANGELOG](./CHANGELOG.md) for the complete control and example breakdown. @@ -65,10 +73,8 @@ The recommended way to add the Unity UI Toolkit Extensions project to your solut The primary method for install is to use OpenUPM to add the package, it can be installed using the following [OpenUPM CLI](https://openupm.com/docs/) command: -> Coming Soon! - -```cli -`openupm add com.unity.uitoolkitextensions` +```shell +openupm add com.unity.uitoolkitextensions ``` > For more details on using [OpenUPM CLI, check the docs here](https://github.com/openupm/openupm-cli#installation). @@ -92,9 +98,9 @@ Just ensure: - The script uses the **Unity.UI.Extensions** namespace so they do not affect any other developments. - (optional) Add Component and Editor options where possible. (editor options are in the Editor\UIToolkitExtensionsMenuOptions.cs file) -## [License](https://raw.githubusercontent.com/Unity-UI-Extensions/com.unity.uitoolkitextensions/release/LICENSE.md) +## [License](./LICENSE) -All scripts conform to the MIT license and are free to use / distribute. See the [LICENSE](https://raw.githubusercontent.com/Unity-UI-Extensions/com.unity.uitoolkitextensions/release/LICENSE.md) file for more information = +All scripts conform to the MIT license and are free to use / distribute. See the [LICENSE](./LICENSE) file for more information. ## [Like what you see?](https://unity-ui-extensions.github.io/FurtherInfo)