Skip to content
Merged
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
43 changes: 43 additions & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Release Please

# On every push to main, Release Please reads conventional commits (feat:, fix:, feat!: ...)
# and keeps a release PR open that bumps <Version> in Directory.Build.props and CHANGELOG.md.
# Merging that PR creates a *draft* GitHub release; release.yml then builds, tests, runs the
# W3C gate, publishes to NuGet, and only then publishes the release (which creates the tag).
# So a failed publish never leaves a public release or tag behind.
#
# Optional: set a RELEASE_PLEASE_TOKEN secret (fine-grained PAT, contents + pull-requests
# write) so the release PR is opened as a user and CI runs on it. With the default
# GITHUB_TOKEN, PRs opened by Actions do not trigger pull_request workflows; release.yml
# re-runs the full suite before publishing, so this is a convenience, not a gate.
on:
push:
branches: [main]

permissions:
contents: write
pull-requests: write
Comment thread
coderabbitai[bot] marked this conversation as resolved.

jobs:
release-please:
runs-on: ubuntu-latest
outputs:
release_created: ${{ steps.rp.outputs.release_created }}
tag_name: ${{ steps.rp.outputs.tag_name }}
sha: ${{ steps.rp.outputs.sha }}
steps:
- uses: googleapis/release-please-action@v4
id: rp
Comment on lines +29 to +30

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Release PR checks stay dormant

release-please-action uses GITHUB_TOKEN, so generated release PRs do not trigger pull-request CI. Review whether branch protection requires those checks before merging.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in 877eff1. main has no branch protection, and release.yml re-runs build, tests, and the W3C gate before anything is published, so release-PR CI is a convenience rather than a gate. Added optional RELEASE_PLEASE_TOKEN (PAT) support on the action so the release PR triggers pull_request CI if we want it; falls back to GITHUB_TOKEN.

with:
token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}

# A tag created with GITHUB_TOKEN does not fire `on: push: tags`, so call the release
# workflow directly instead of relying on the tag trigger.
publish:
needs: release-please
if: ${{ needs.release-please.outputs.release_created == 'true' }}
uses: ./.github/workflows/release.yml
with:
tag: ${{ needs.release-please.outputs.tag_name }}
sha: ${{ needs.release-please.outputs.sha }}
secrets: inherit
Comment on lines +36 to +43

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Release precedes publication checks

release-please-action creates the tag and GitHub release before build and test completion. A later failure leaves an unpublished GitHub release.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in 877eff1. Release Please now creates a draft release ("draft": true), which does not create the tag. release.yml checks out the release sha, runs build/test/W3C gate, pushes to NuGet, and only then runs gh release edit --draft=false, which creates the tag. A failure at any step leaves no public release or tag.

52 changes: 38 additions & 14 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
name: Release

permissions:
contents: read
contents: write # publish the draft GitHub release after NuGet push
id-token: write # NuGet Trusted Publishing (OIDC)

# Tag the release commit `v<Version>` (Version lives in Directory.Build.props).
# Normally invoked by release-please.yml after a release PR is merged. Pushing a
# `v<Version>` tag by hand (Version lives in Directory.Build.props) also works.
on:
push:
tags: ['v*']
workflow_call:
inputs:
tag:
description: Release tag, e.g. v0.1.0-alpha
required: true
type: string
sha:
description: Commit to release. The draft release's tag does not exist yet.
required: true
type: string

env:
DOTNET_NOLOGO: true
Expand All @@ -20,6 +32,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha || github.ref_name }}
fetch-depth: 0

- name: Set up .NET
Expand All @@ -31,9 +44,10 @@ jobs:

- name: Verify the tag matches <Version>
run: |
tag="${{ inputs.tag || github.ref_name }}"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
version=$(sed -n 's:.*<Version>\(.*\)</Version>.*:\1:p' Directory.Build.props | head -1)
if [ "v$version" != "$GITHUB_REF_NAME" ]; then
echo "::error::Tag '$GITHUB_REF_NAME' does not match <Version>$version</Version> in Directory.Build.props."
if [ "v$version" != "$tag" ]; then
echo "::error::Tag '$tag' does not match <Version>$version</Version> in Directory.Build.props."
exit 1
fi

Expand Down Expand Up @@ -75,18 +89,28 @@ jobs:
artifacts/*.snupkg
if-no-files-found: error

# No NUGET_API_KEY secret => nothing is published; the packed artifacts above still
# upload, so a tag on a fork or before the secret exists is a harmless dry run.
# (The `secrets` context is not available in a step-level `if`, hence the env check.)
# Trusted Publishing: nuget.org has a policy for statelyai/xstate-csharp + release.yml
# (owner davidkpiano, packages XState*). NuGet/login exchanges the GitHub OIDC token
# for a short-lived API key; no stored secret.
- name: NuGet login (OIDC)
id: nuget-login
if: ${{ github.repository == 'statelyai/xstate-csharp' }}
uses: NuGet/login@v1
with:
user: davidkpiano

- name: Push to NuGet
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
if: ${{ github.repository == 'statelyai/xstate-csharp' }}
run: |
if [ -z "$NUGET_API_KEY" ]; then
echo "::notice::NUGET_API_KEY is not configured; skipping publish."
exit 0
fi
dotnet nuget push "artifacts/*.nupkg" \
--api-key "$NUGET_API_KEY" \
--api-key "${{ steps.nuget-login.outputs.NUGET_API_KEY }}" \
--source https://api.nuget.org/v3/index.json \
--skip-duplicate

# Called from release-please.yml: the release is still a draft (no tag yet). Publishing it
# creates the tag, so a failed build/test/push above leaves nothing public behind.
- name: Publish GitHub release
if: ${{ inputs.tag != '' }}
env:
GH_TOKEN: ${{ github.token }}
run: gh release edit "${{ inputs.tag }}" --repo "$GITHUB_REPOSITORY" --draft=false
3 changes: 3 additions & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
".": "0.1.0-alpha"
}
63 changes: 63 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Contributing

## Build and test

Requires the .NET 10 SDK (pinned in `global.json`).

```bash
dotnet build XState.slnx
dotnet test XState.slnx
```

CI runs the same on every PR and push to `main`. The W3C SCXML suite must match
[CONFORMANCE.md](CONFORMANCE.md) exactly: an unexpected pass fails the run, same as an
unexpected failure. Update the `KnownFailing` list and CONFORMANCE.md together.

## Commit messages

Use [Conventional Commits](https://www.conventionalcommits.org/). Release Please reads
them to pick the next version and write the changelog:

| Prefix | Effect |
|---|---|
| `fix:` | patch bump, listed under Bug Fixes |
| `feat:` | minor bump, listed under Features |
| `feat!:` or `BREAKING CHANGE:` footer | major bump (while pre-1.0: minor) |
| `chore:`, `docs:`, `ci:`, `test:`, `refactor:` | no release, not in changelog |

Squash-merge PRs and make the squash title the conventional commit.

## Releasing

Releases are automated. Do not edit `<Version>` in `Directory.Build.props` or
`CHANGELOG.md` by hand.

1. Merge PRs to `main` as usual.
2. Release Please keeps a PR open titled `chore(main): release <version>`. It bumps
`<Version>` and updates `CHANGELOG.md`. Review the changelog there.
3. Merge that PR. This creates a draft GitHub release and runs `release.yml`, which builds,
tests, runs the W3C gate, packs, and pushes `XState` and `XState.Scxml` to nuget.org.
4. On success the workflow publishes the release, which creates the `v<version>` tag.
On failure nothing is public: fix on `main`, then re-run the failed workflow.

### Versioning

Pre-1.0: `feat:` bumps minor, `fix:` bumps patch. Versions are alpha prereleases
(`0.1.0-alpha`, ...). To go stable, remove `prerelease`, `prerelease-type`, and
`versioning` from `release-please-config.json`.

### Publishing credentials

NuGet uses Trusted Publishing (OIDC): nuget.org holds a policy for
`statelyai/xstate-csharp` + `release.yml`, packages `XState*`, owner `davidkpiano`.
No API key secret exists. `NuGet/login@v1` exchanges the workflow's OIDC token for a
short-lived key. Manage the policy at https://www.nuget.org/account/trustedpublishing.

Optional: a `RELEASE_PLEASE_TOKEN` repo secret (fine-grained PAT with contents and
pull-requests write) makes the release PR open as a user so CI runs on it. Without it the
PR still works; `release.yml` runs the full suite before publishing regardless.

### Manual release

Pushing a `v<Version>` tag matching `Directory.Build.props` also runs `release.yml`.
Use only if the automation is broken.
4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@

<!-- Package metadata (shared by both shipping libraries; test projects set IsPackable=false) -->
<PropertyGroup>
<!-- Release version. Bump HERE, then tag the commit `v<Version>` to trigger
.github/workflows/release.yml, which packs and pushes to NuGet. -->
<!-- Release version. Managed by Release Please (.github/workflows/release-please.yml):
merge the release PR to bump this, tag, and publish to NuGet. -->
<Version>0.1.0-alpha</Version>
<Authors>Stately</Authors>
<Company>Stately</Company>
Expand Down
20 changes: 20 additions & 0 deletions release-please-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"release-type": "simple",
"draft": true,
"include-component-in-tag": false,
"bump-minor-pre-major": true,
"bump-patch-for-minor-pre-major": true,
"prerelease": true,
"prerelease-type": "alpha",
"versioning": "prerelease",
"packages": {
".": {
"package-name": "xstate",
"changelog-path": "CHANGELOG.md",
"extra-files": [
{ "type": "xml", "path": "Directory.Build.props", "xpath": "//Version" }
]
}
}
}
Loading