diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md new file mode 100644 index 00000000..fa9b8dc8 --- /dev/null +++ b/.claude/skills/release/SKILL.md @@ -0,0 +1,40 @@ +--- +name: release +description: Create a release PR with version bump and changelog +disable-model-invocation: true +argument-hint: version +--- + +Create a PR with branch name `release/v$ARGUMENTS` containing only these changes: + +1. Update version in `pyproject.toml` +2. Ensure `TONGUES_VERSION` matches between `.github/workflows/ci.yml` and `.github/workflows/release.yml` +3. Add a new version column to the Releases table in `README.md` with download links (keep the previous version column) + +No other changes—no refactors, no fixes, no documentation updates. + +## Changelog + +Generate release notes from commits since the last tag (or all commits if no tags): +``` +git log $(git describe --tags --abbrev=0 2>/dev/null || git rev-list --max-parents=0 HEAD)..HEAD --oneline +``` + +Focus on what matters to users: +- New features and capabilities +- Breaking changes or behavior changes +- Group all bug fixes as "Various bug fixes" (don't itemize) +- Omit internal refactors, test changes, and CI updates + +Put the changelog in the PR body. The workflow extracts it for the GitHub release. + +Run `just check` before pushing. PR title: `Release v$ARGUMENTS` + +## After merge + +Tag, push, and clean up: +``` +git checkout main && git pull && git tag v$ARGUMENTS && git push --tags && git push origin --delete release/v$ARGUMENTS +``` + +The tag triggers a workflow that creates the GitHub release with transpiled binaries (parable.py, parable.js). diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..fb50a0d2 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,83 @@ +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + + - name: Extract version from tag + id: version + run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + + - name: Verify version matches pyproject.toml + run: | + toml_version=$(grep -oP 'version = "\K[^"]+' pyproject.toml) + if [ "$toml_version" != "${{ steps.version.outputs.version }}" ]; then + echo "Tag version (${{ steps.version.outputs.version }}) does not match pyproject.toml ($toml_version)" + exit 1 + fi + + - name: Get release notes from PR + id: notes + env: + GH_TOKEN: ${{ github.token }} + run: | + body=$(gh pr list --state merged --head "release/v${{ steps.version.outputs.version }}" --json body --jq '.[0].body // empty') + if [ -z "$body" ]; then + echo "No release PR found, using auto-generated notes" + echo "auto=true" >> $GITHUB_OUTPUT + else + echo "body<> $GITHUB_OUTPUT + echo "$body" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + fi + + - uses: astral-sh/setup-uv@v4 + + - uses: extractions/setup-just@v2 + + - uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install Tongues + env: + TONGUES_VERSION: v0.2.4 + run: | + mkdir -p "$HOME/.local/lib/tongues/lib" "$HOME/.local/lib/tongues/bin" "$HOME/.local/bin" + curl -sSfL "https://github.com/ldayton/Tongues/releases/download/${TONGUES_VERSION}/tongues.js" \ + -o "$HOME/.local/lib/tongues/lib/tongues.js" + curl -sSfL "https://raw.githubusercontent.com/ldayton/Tongues/${TONGUES_VERSION}/tongues/bin/tongues.js" \ + -o "$HOME/.local/lib/tongues/bin/tongues.js" + printf '#!/bin/sh\nexec node %s/.local/lib/tongues/bin/tongues.js "$@"\n' "$HOME" > "$HOME/.local/bin/tongues" + chmod +x "$HOME/.local/bin/tongues" + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + + - name: Run checks + run: just check + + - name: Transpile release binaries + run: | + mkdir -p .out + tongues --target python -o .out/parable.py src/parable.py + tongues --target javascript -o .out/parable.js src/parable.py + echo 'module.exports = { parse, ParseError, MatchedPairError };' >> .out/parable.js + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + body: ${{ steps.notes.outputs.body }} + generate_release_notes: ${{ steps.notes.outputs.auto == 'true' }} + files: | + .out/parable.py + .out/parable.js diff --git a/README.md b/README.md index d02df96c..eedc819e 100644 --- a/README.md +++ b/README.md @@ -24,12 +24,12 @@ Parse bash exactly as bash does. One file, zero dependencies, in your language. All releases, even Python, run through the transpiler. Current languages: -| Language | Min Version | -| ---------- | ----------------- | -| Javascript | Node.js 21 | -| Python | CPython/Pypy 3.12 | +| Language | Min Version | v0.1.0 | +| ---------- | ----------------- | ------------------------------------------------------------------------------------ | +| Javascript | Node.js 21 | [parable.js](https://github.com/ldayton/Parable/releases/download/v0.1.0/parable.js) | +| Python | CPython/Pypy 3.12 | [parable.py](https://github.com/ldayton/Parable/releases/download/v0.1.0/parable.py) | -Output code quality is a work in progress. Currently the transpiler prioritizes correctness over readability; generated code may not yet match hand-written idioms, yet. +Caveats: output code quality is a work in progress. Currently the transpiler prioritizes correctness over readability; generated code may not yet match hand-written idioms, yet. Additionally, the project's fuzzer still finds obscure edge cases where Parable differs from GNU Bash. While Parable matches bash for essentially 100% of real world examples, we're still shaking out remaining theoretical differences. ## Why Parable?