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
28 changes: 28 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Dependency updates never ship to users, so they use the non-releasing "build" commit type.
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
cooldown:
default-days: 7
commit-message:
prefix: build
include: scope
groups:
github-actions:
patterns: ["*"]

- package-ecosystem: nuget
directory: /
schedule:
interval: weekly
cooldown:
default-days: 7
commit-message:
prefix: build
include: scope
groups:
nuget:
patterns: ["*"]
67 changes: 67 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Release

on:
workflow_dispatch:
inputs:
version:
description: Version to release (MAJOR.MINOR.PATCH). Leave empty to derive it from the Conventional Commits since the last release.
required: false
type: string
dry-run:
description: Only show the proposed version without releasing.
required: false
type: boolean
default: true

permissions:
contents: read

concurrency:
group: release
cancel-in-progress: false

jobs:
validate:
# Runs third-party npm code, so it must not hold a write token.
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
- name: Validate Claude Code plugin
env:
DISABLE_AUTOUPDATER: "1"
run: npx --yes @anthropic-ai/claude-code@latest plugin validate . --strict

release:
needs: validate
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# The release tool needs the full history and all tags to propose the next version.
fetch-depth: 0
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
global-json-file: global.json
- name: Configure git identity
run: |
git config user.name "github-actions[bot]"
git config user.email "41898208+github-actions[bot]@users.noreply.github.com"
- name: Release
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ inputs.version }}
DRY_RUN: ${{ inputs.dry-run }}
run: |
args=(--yes)
if [ -n "$VERSION" ]; then args+=(--version "$VERSION"); fi
if [ "$DRY_RUN" = "true" ]; then args+=(--dry-run); fi
dotnet run --project tools/GuidedCoding.Release -- "${args[@]}"
6 changes: 3 additions & 3 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-dotnet@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
global-json-file: global.json
- run: dotnet run --project tools/GuidedCoding.ClaudeGenerator --configuration Release -- --check
Expand All @@ -23,7 +23,7 @@ jobs:
run: gh skill publish --dry-run
env:
GH_TOKEN: ${{ github.token }}
- uses: actions/setup-node@v6
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
- name: Validate Claude Code plugin
Expand Down
16 changes: 13 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,26 @@ This repository contains skills for Guided Coding. The root package supports the

## Manifests and versions

Keep the version synchronized across `plugin.json`, `claude-plugin/.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`, and release tags.
- Keep the version synchronized across `plugin.json`, `claude-plugin/.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`, and `MAJOR.MINOR.PATCH` release tags without a `v` prefix. Every tag is a release tag: the release tool refuses to run while any tag has another form.
- Do not bump versions manually. Between releases, the manifests hold the last released version. Release with `dotnet run --project tools/GuidedCoding.Release` or the manually triggered Release workflow in GitHub Actions. The tool derives the next version from the commits since the last release tag, updates the manifests and `CHANGELOG.md`, validates, commits, tags, pushes, and runs `gh skill publish`.
- Describe user-facing changes under `## [Unreleased]` in `CHANGELOG.md`.

Use Conventional Commits messages.
## Commit messages

Use Conventional Commits messages. The commit type decides the next version:

- `feat` releases a minor version, `fix` and `perf` release a patch version.
- `!` after the type or a `BREAKING CHANGE:` footer releases a major version. Removing or renaming a skill is a breaking change.
- Other types, such as `docs`, `test`, `refactor`, and `chore`, do not trigger a release. Changes to shipped skill content are therefore `feat` or `fix`, never `docs` or `chore`.
- Dependency updates do not reach users, so Dependabot commits as `build(deps)`. Keep this prefix in `.github/dependabot.yml`.
- Pin third-party actions in workflows to a commit SHA with a version comment. Dependabot keeps both up to date.

## Feedback loops

- Regenerate the Claude adapter and run `dotnet test` after changing skills or manifests.
- Run `dotnet run --project tools/GuidedCoding.ClaudeGenerator -- --check` to detect drift.
- Run `claude plugin validate . --strict` when Claude Code is installed.
- Run `gh skill publish --dry-run` before publishing a release.
- Run `gh skill publish --dry-run` before publishing a release. The release tool runs it for you.

## This is your space

Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

All notable changes to Guided Coding are documented here.

## [2.0.0] - Unreleased
## [Unreleased]

- Add UTC timestamps to plan and Plan Deviations filenames.
- Treat `ai-plans/` as an append-only, event-sourced decision record.
Expand Down
10 changes: 8 additions & 2 deletions GuidedCoding.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
<File Path="plugin.json" />
<File Path="README.md" />
</Folder>
<Project Path="tests/GuidedCoding.Tests/GuidedCoding.Tests.csproj" />
<Project Path="tools/GuidedCoding.ClaudeGenerator/GuidedCoding.ClaudeGenerator.csproj" />
<Folder Name="/tests/">
<Project Path="tests/GuidedCoding.Release.Tests/GuidedCoding.Release.Tests.csproj" />
<Project Path="tests/GuidedCoding.Tests/GuidedCoding.Tests.csproj" />
</Folder>
<Folder Name="/tools/">
<Project Path="tools/GuidedCoding.ClaudeGenerator/GuidedCoding.ClaudeGenerator.csproj" />
<Project Path="tools/GuidedCoding.Release/GuidedCoding.Release.csproj" />
</Folder>
</Solution>
34 changes: 32 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,5 +124,35 @@ claude plugin validate . --strict
gh skill publish --dry-run
```

Keep `plugin.json`, `claude-plugin/.claude-plugin/plugin.json`,
`.claude-plugin/marketplace.json`, and the release tag on the same Semantic Version.
### Releasing

Releases are made from a `main` branch that matches `origin/main`, so the release pushes nothing
but its own commit. Describe the changes under `## [Unreleased]` in `CHANGELOG.md` first, then run:

```sh
dotnet run --project tools/GuidedCoding.Release -- --dry-run # show the proposed version
dotnet run --project tools/GuidedCoding.Release # release the proposed version
dotnet run --project tools/GuidedCoding.Release -- --version 3.0.0 # release a specific version
```

The tool derives the next version from the Conventional Commits since the last `MAJOR.MINOR.PATCH`
tag: breaking changes release a major version, `feat` a minor version, and `fix` or `perf` a patch
version. After you confirm, it updates `plugin.json`, `claude-plugin/.claude-plugin/plugin.json`,
`.claude-plugin/marketplace.json`, and `CHANGELOG.md`, runs the validations, commits, tags, pushes
`main` together with the tag, and publishes the Agent Skills with `gh skill publish`. Pushing the new
version to `main` releases the Claude Code plugin. The first release needs `--version` because there
is no release tag to start from. Pass `--yes` to skip the confirmation.

Every tag in the repository must be a plain `MAJOR.MINOR.PATCH` version, and the tool refuses to
release while any other tag exists. If you ever publish by hand, pass `--tag MAJOR.MINOR.PATCH` to
`gh skill publish`, because its interactive prompt suggests `v`-prefixed tags.

The repository must carry the `agent-skills` topic for `gh skill publish`. It is already set; the
tool does not set it because GitHub's workflow token lacks the required admin permission.

#### Releasing from GitHub Actions

Maintainers with write access can also release from the **Actions** tab by running the **Release**
workflow on `main`. It runs the same tool with `--yes`. Leave **Version** empty to use the proposed
version. **Dry run** is enabled by default, so a first run only shows the proposal; run the workflow
again with **Dry run** unchecked to release.
12 changes: 12 additions & 0 deletions tests/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />

<PropertyGroup>
<OutputType>Exe</OutputType>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="xunit.v3.mtp-v2" Version="3.2.2" />
</ItemGroup>
</Project>
62 changes: 62 additions & 0 deletions tests/GuidedCoding.Release.Tests/ChangelogTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System;
using Xunit;

namespace GuidedCoding.Release.Tests;

public sealed class ChangelogTests
{
private static readonly SemanticVersion Version = new (1, 1, 0);
private static readonly DateOnly Date = new (2026, 9, 26);

[Fact]
public void MovesTheUnreleasedEntriesIntoTheReleasedVersion()
{
const string changelog =
"# Changelog\n\n## [Unreleased]\n\n- Add a skill.\n- Fix a typo.\n\n## [1.0.0] - 2026-01-01\n\n- Start.\n";

var released = Changelog.Release(changelog, Version, Date);

Assert.Equal(
"# Changelog\n\n## [Unreleased]\n\n## [1.1.0] - 2026-09-26\n\n- Add a skill.\n- Fix a typo.\n\n" +
"## [1.0.0] - 2026-01-01\n\n- Start.\n",
released
);
}

[Fact]
public void KeepsSubsectionHeadingsWithTheirEntries()
{
var released = Changelog.Release("## [Unreleased]\n\n### Added\n\n- Add a skill.\n", Version, Date);

Assert.Equal("## [Unreleased]\n\n## [1.1.0] - 2026-09-26\n\n### Added\n\n- Add a skill.\n", released);
}

[Fact]
public void NormalizesLineEndings()
{
var released = Changelog.Release("## [Unreleased]\r\n\r\n- Add a skill.\r\n", Version, Date);

Assert.Equal("## [Unreleased]\n\n## [1.1.0] - 2026-09-26\n\n- Add a skill.\n", released);
}

[Fact]
public void RequiresAnUnreleasedSection()
{
var exception = Assert.Throws<InvalidOperationException>(
() => Changelog.Release("# Changelog\n\n## [1.0.0] - 2026-01-01\n\n- Start.\n", Version, Date)
);

Assert.Equal("CHANGELOG.md has no '## [Unreleased]' section.", exception.Message);
}

[Theory]
[InlineData("# Changelog\n\n## [Unreleased]\n")]
[InlineData("# Changelog\n\n## [Unreleased]\n\n \n## [1.0.0] - 2026-01-01\n\n- Start.\n")]
[InlineData("# Changelog\n\n## [Unreleased]\n\n### Added\n\n### Fixed\n\n## [1.0.0] - 2026-01-01\n\n- Start.\n")]
public void RequiresUnreleasedEntries(string changelog)
{
var exception = Assert.Throws<InvalidOperationException>(() => Changelog.Release(changelog, Version, Date));

Assert.StartsWith("CHANGELOG.md has no entries under '## [Unreleased]'.", exception.Message);
}
}
52 changes: 52 additions & 0 deletions tests/GuidedCoding.Release.Tests/ConventionalCommitsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Xunit;

namespace GuidedCoding.Release.Tests;

public sealed class ConventionalCommitsTests
{
[Theory]
[InlineData("feat!: rename a skill")]
[InlineData("fix(setup)!: drop an option")]
[InlineData("refactor!: restructure the plugin")]
[InlineData("docs: explain a skill\n\nBREAKING CHANGE: the skill was renamed")]
[InlineData("fix: correct a typo\n\nBREAKING-CHANGE: the output moved")]
[InlineData("feat: add a skill\r\n\r\nBREAKING CHANGE: the skill replaces another one")]
public void ClassifiesBreakingChangesAsMajor(string message)
{
Assert.Equal(ReleaseType.Major, ConventionalCommits.Classify(message));
}

[Theory]
[InlineData("feat: add a skill")]
[InlineData("feat(setup): support another agent")]
[InlineData("Feat: add a skill")]
[InlineData("feat: add a skill\n\nThe body mentions BREAKING CHANGE: only inline.")]
public void ClassifiesFeaturesAsMinor(string message)
{
Assert.Equal(ReleaseType.Minor, ConventionalCommits.Classify(message));
}

[Theory]
[InlineData("fix: correct a typo")]
[InlineData("fix(write-plan): clarify a step")]
[InlineData("perf: shorten a skill")]
[InlineData("fix: correct a typo\n\nbreaking change: footers must be uppercase")]
public void ClassifiesFixesAndPerformanceImprovementsAsPatch(string message)
{
Assert.Equal(ReleaseType.Patch, ConventionalCommits.Classify(message));
}

[Theory]
[InlineData("docs: explain a skill")]
[InlineData("chore(release): 1.0.0")]
[InlineData("test: cover a skill")]
[InlineData("Merge pull request #1 from feO2x/guided-learning")]
[InlineData("feat:missing space")]
[InlineData("feat add a skill")]
[InlineData("Add a skill\n\nBREAKING CHANGE: the subject is not conventional")]
[InlineData("")]
public void ClassifiesOtherCommitsAsNone(string message)
{
Assert.Equal(ReleaseType.None, ConventionalCommits.Classify(message));
}
}
Loading
Loading