diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1dfc0ef --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Type check + run: bun run typecheck + + - name: Run tests with coverage + run: bun test --coverage --coverage-reporter=lcov + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage/lcov.info + flags: unittests + name: codecov-umbrella + fail_ci_if_error: true + verbose: true diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml new file mode 100644 index 0000000..a20936a --- /dev/null +++ b/.github/workflows/release-pr.yml @@ -0,0 +1,272 @@ +name: Create Release PR + +on: + workflow_dispatch: + inputs: + version_type: + description: 'Version bump type' + required: true + type: choice + options: + - patch + - minor + - major + - auto + default: 'auto' + custom_version: + description: 'Custom version (overrides version_type if set, e.g., 1.2.3)' + required: false + type: string + +permissions: + contents: write + pull-requests: write + +jobs: + create-release-pr: + runs-on: ubuntu-latest + + steps: + - name: Checkout main branch + uses: actions/checkout@v4 + with: + ref: main + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Get current version + id: current + run: | + CURRENT_VERSION=$(jq -r '.version' package.json) + echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + echo "Current version: $CURRENT_VERSION" + + - name: Determine version bump from commits + id: analyze + if: inputs.version_type == 'auto' && inputs.custom_version == '' + run: | + # Get commits since last tag + LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + + if [ -z "$LAST_TAG" ]; then + COMMITS=$(git log --oneline) + else + COMMITS=$(git log --oneline ${LAST_TAG}..HEAD) + fi + + echo "Commits since last tag:" + echo "$COMMITS" + + # Analyze commits for version bump (conventional commits) + BUMP="patch" + + if echo "$COMMITS" | grep -qiE "^[a-f0-9]+ (feat|feature)(\(.+\))?!:|BREAKING CHANGE"; then + BUMP="major" + elif echo "$COMMITS" | grep -qiE "^[a-f0-9]+ (feat|feature)(\(.+\))?:"; then + BUMP="minor" + fi + + echo "bump=$BUMP" >> $GITHUB_OUTPUT + echo "Determined bump type: $BUMP" + + - name: Calculate new version + id: version + run: | + CURRENT="${{ steps.current.outputs.version }}" + + # Use custom version if provided + if [ -n "${{ inputs.custom_version }}" ]; then + CUSTOM="${{ inputs.custom_version }}" + # Strip optional 'v' prefix and validate strict semver (X.Y.Z) + CUSTOM="${CUSTOM#v}" + if [[ ! "$CUSTOM" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::Invalid custom version '${{ inputs.custom_version }}'. Must be semver (e.g., 1.2.3 or v1.2.3)." + exit 1 + fi + NEW_VERSION="$CUSTOM" + else + # Determine bump type + if [ "${{ inputs.version_type }}" == "auto" ]; then + BUMP="${{ steps.analyze.outputs.bump }}" + else + BUMP="${{ inputs.version_type }}" + fi + + # Split version into parts + IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT" + + # Validate no prerelease suffix (e.g., 1.0.0-alpha.1) + if [[ ! "$MAJOR" =~ ^[0-9]+$ ]] || [[ ! "$MINOR" =~ ^[0-9]+$ ]] || [[ ! "$PATCH" =~ ^[0-9]+$ ]]; then + echo "::error::Prerelease versions not supported (got: $CURRENT). Use explicit version input instead." + exit 1 + fi + + case "$BUMP" in + major) + NEW_VERSION="$((MAJOR + 1)).0.0" + ;; + minor) + NEW_VERSION="${MAJOR}.$((MINOR + 1)).0" + ;; + patch) + NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))" + ;; + esac + fi + + echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT + echo "New version: $NEW_VERSION" + + - name: Create release branch + run: | + BRANCH_NAME="release-v${{ steps.version.outputs.new_version }}" + git checkout -b "$BRANCH_NAME" + echo "Created branch: $BRANCH_NAME" + + - name: Update package.json version + run: | + NEW_VERSION="${{ steps.version.outputs.new_version }}" + jq --arg v "$NEW_VERSION" '.version = $v' package.json > package.json.tmp + mv package.json.tmp package.json + echo "Updated package.json to version $NEW_VERSION" + + - name: Generate changelog entry + id: changelog + run: | + NEW_VERSION="${{ steps.version.outputs.new_version }}" + TODAY=$(date +%Y-%m-%d) + LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + + # Get commits grouped by type + if [ -z "$LAST_TAG" ]; then + COMMITS=$(git log --pretty=format:"%s" HEAD) + else + COMMITS=$(git log --pretty=format:"%s" ${LAST_TAG}..HEAD) + fi + + # Build changelog entry + ENTRY="## [$NEW_VERSION] - $TODAY"$'\n\n' + + # Features (normalize case before sed to handle Feat/FEAT variants) + FEATURES=$(echo "$COMMITS" | grep -iE "^feat(\(.+\))?:" | tr '[:upper:]' '[:lower:]' | sed 's/^feat(\([^)]*\)): /- **\1**: /' | sed 's/^feat: /- /' || true) + if [ -n "$FEATURES" ]; then + ENTRY+="### Added"$'\n'"$FEATURES"$'\n\n' + fi + + # Fixes (normalize case before sed to handle Fix/FIX variants) + FIXES=$(echo "$COMMITS" | grep -iE "^fix(\(.+\))?:" | tr '[:upper:]' '[:lower:]' | sed 's/^fix(\([^)]*\)): /- **\1**: /' | sed 's/^fix: /- /' || true) + if [ -n "$FIXES" ]; then + ENTRY+="### Fixed"$'\n'"$FIXES"$'\n\n' + fi + + # Other changes (refactor, perf, style, or non-conventional commits) + OTHERS=$(echo "$COMMITS" | grep -ivE "^(feat|fix|chore|ci|docs|test)(\(.+\))?:" | sed 's/^/- /' || true) + if [ -n "$OTHERS" ]; then + ENTRY+="### Changed"$'\n'"$OTHERS"$'\n\n' + fi + + # Save entry to file for use in PR body + echo "$ENTRY" > /tmp/changelog_entry.md + + # Prepend to CHANGELOG.md if it exists + # Handles [Unreleased] section: insert new version AFTER it + if [ -f "CHANGELOG.md" ]; then + # Check for [Unreleased] section (case-insensitive) + UNRELEASED_LINE=$(grep -in '^## \[Unreleased\]' CHANGELOG.md | head -1 | cut -d: -f1) + # Find first versioned section (## [x.y.z]) + FIRST_VERSION_LINE=$(grep -n '^## \[[0-9]' CHANGELOG.md | head -1 | cut -d: -f1) + + if [ -n "$UNRELEASED_LINE" ]; then + # Has [Unreleased] section - insert new version after it + head -n "$UNRELEASED_LINE" CHANGELOG.md > /tmp/changelog_new.md + echo "" >> /tmp/changelog_new.md + cat /tmp/changelog_entry.md >> /tmp/changelog_new.md + tail -n +"$((UNRELEASED_LINE + 1))" CHANGELOG.md >> /tmp/changelog_new.md + elif [ -n "$FIRST_VERSION_LINE" ]; then + # No [Unreleased], insert before first version section + head -n "$((FIRST_VERSION_LINE - 1))" CHANGELOG.md > /tmp/changelog_new.md + echo "" >> /tmp/changelog_new.md + cat /tmp/changelog_entry.md >> /tmp/changelog_new.md + tail -n +"$FIRST_VERSION_LINE" CHANGELOG.md >> /tmp/changelog_new.md + else + # No version sections found, append to end + cat CHANGELOG.md > /tmp/changelog_new.md + echo "" >> /tmp/changelog_new.md + cat /tmp/changelog_entry.md >> /tmp/changelog_new.md + fi + mv /tmp/changelog_new.md CHANGELOG.md + echo "Updated CHANGELOG.md" + fi + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Commit changes + run: | + NEW_VERSION="${{ steps.version.outputs.new_version }}" + git add package.json + [ -f CHANGELOG.md ] && git add CHANGELOG.md + git commit -m "chore(release): prepare v$NEW_VERSION" + + - name: Push branch + run: | + BRANCH_NAME="release-v${{ steps.version.outputs.new_version }}" + git push -u origin "$BRANCH_NAME" + + - name: Create Pull Request + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + NEW_VERSION="${{ steps.version.outputs.new_version }}" + CURRENT_VERSION="${{ steps.current.outputs.version }}" + + # Create PR body + cat << 'EOF' > /tmp/pr_body.md + ## Release v$NEW_VERSION + + This PR prepares the release of version **$NEW_VERSION** (from $CURRENT_VERSION). + + ### Changes included + + EOF + + # Append changelog entry if exists + if [ -f /tmp/changelog_entry.md ]; then + cat /tmp/changelog_entry.md >> /tmp/pr_body.md + fi + + cat << 'EOF' >> /tmp/pr_body.md + + --- + + ### Checklist + + - [ ] Version bump is correct + - [ ] CHANGELOG.md is accurate + - [ ] All tests pass + + ### After merge + + When this PR is merged: + 1. A git tag `v$NEW_VERSION` will be created automatically + 2. A GitHub release will be published + 3. The package will be published to npm + EOF + + # Replace version placeholders + sed -i "s/\$NEW_VERSION/$NEW_VERSION/g" /tmp/pr_body.md + sed -i "s/\$CURRENT_VERSION/$CURRENT_VERSION/g" /tmp/pr_body.md + + gh pr create \ + --title "Release v$NEW_VERSION" \ + --body-file /tmp/pr_body.md \ + --base main \ + --label "release" \ + --label "automated" diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml new file mode 100644 index 0000000..472e535 --- /dev/null +++ b/.github/workflows/release-publish.yml @@ -0,0 +1,140 @@ +name: Publish Release + +on: + pull_request: + types: [closed] + branches: [main] + +permissions: + contents: write + id-token: write + +jobs: + publish: + # Only run if PR was merged and has release label + if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release') + runs-on: ubuntu-latest + environment: publish # Required for npm OIDC trusted publisher + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + # Pin to merge commit SHA to avoid race condition with new commits + ref: ${{ github.event.pull_request.merge_commit_sha }} + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Setup Node.js (for npm publish) + uses: actions/setup-node@v4 + with: + node-version: '20' + registry-url: 'https://registry.npmjs.org' + + - name: Update npm for OIDC trusted publishing + run: npm install -g npm@latest + + - name: Install dependencies + run: bun install + + - name: Build + run: bun run build + + - name: Run tests + run: bun test + + - name: Get version from package.json + id: version + run: | + VERSION=$(jq -r '.version' package.json) + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Release version: $VERSION" + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Create and push tag + run: | + VERSION="${{ steps.version.outputs.version }}" + TAG="v$VERSION" + + # Check if tag already exists + if git rev-parse "$TAG" >/dev/null 2>&1; then + echo "Tag $TAG already exists, skipping tag creation" + else + git tag -a "$TAG" -m "Release $TAG" + git push origin "$TAG" + echo "Created and pushed tag: $TAG" + fi + + - name: Generate release notes + id: release_notes + run: | + VERSION="${{ steps.version.outputs.version }}" + + # Get previous tag (exclude current version to find actual previous release) + PREV_TAG=$(git describe --tags --abbrev=0 --exclude="v${VERSION}" HEAD 2>/dev/null || echo "") + + # Build release notes + cat << EOF > /tmp/release_notes.md + ## What's Changed + + EOF + + # Get commits for this release + if [ -n "$PREV_TAG" ]; then + git log --pretty=format:"- %s (%h)" ${PREV_TAG}..HEAD >> /tmp/release_notes.md + echo "" >> /tmp/release_notes.md + echo "" >> /tmp/release_notes.md + echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREV_TAG}...v${VERSION}" >> /tmp/release_notes.md + else + git log --pretty=format:"- %s (%h)" >> /tmp/release_notes.md + fi + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION="${{ steps.version.outputs.version }}" + TAG="v$VERSION" + + # Check if release already exists + if gh release view "$TAG" >/dev/null 2>&1; then + echo "Release $TAG already exists, skipping release creation" + else + gh release create "$TAG" \ + --title "$TAG" \ + --notes-file /tmp/release_notes.md + echo "Created GitHub release: $TAG" + fi + + - name: Publish to npm + # Uses OIDC trusted publisher - no NPM_TOKEN needed + run: | + VERSION="${{ steps.version.outputs.version }}" + PACKAGE_NAME="opencode-dotenv" + + # Check if this version is already published on npm (idempotency) + if npm view "$PACKAGE_NAME@$VERSION" >/dev/null 2>&1; then + echo "Version $PACKAGE_NAME@$VERSION already exists on npm, skipping publish" + else + npm publish --provenance --access public + echo "Published to npm: $PACKAGE_NAME@$VERSION" + fi + + - name: Post release summary + run: | + VERSION="${{ steps.version.outputs.version }}" + echo "## Release Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Version**: $VERSION" >> $GITHUB_STEP_SUMMARY + echo "- **Git Tag**: v$VERSION" >> $GITHUB_STEP_SUMMARY + echo "- **GitHub Release**: https://github.com/${{ github.repository }}/releases/tag/v$VERSION" >> $GITHUB_STEP_SUMMARY + echo "- **npm Package**: https://www.npmjs.com/package/opencode-dotenv/v/$VERSION" >> $GITHUB_STEP_SUMMARY diff --git a/CHANGELOG.md b/CHANGELOG.md index 484aad5..50b213a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added +- CI workflow with tests and Codecov coverage +- Automated release PR workflow +- Automated publish workflow on PR merge (npm OIDC) +- Makefile workflow helpers (`wf-release`, `wf-ci`, etc.) +- Repository metadata in `package.json` + +### Changed +- Updated `RELEASE.md` with automated release process documentation + ## [0.5.1] - 2026-01-08 ### Added @@ -46,22 +58,87 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Resolved `TypeError: hook.config is not a function` error - Clean exports prevent OpenCode from treating utilities as hooks -## [0.4.1] - Previous +## [0.4.1] - 2026-01-05 + +### Fixed +- Handle non-string content in config parser to prevent crash + +## [0.4.0] - 2026-01-05 + +### Added +- Comprehensive dotenv parsing with full test coverage +- Support for quoted values (single and double quotes) +- Support for inline comments +- Support for multiline values + +## [0.3.8] - 2026-01-05 + +### Fixed +- Revert explicit hooks to avoid trace trap crash on OpenCode startup + +## [0.3.7] - 2026-01-05 + +### Added +- Explicit empty hooks structure for OpenCode compatibility -- Bug fixes and improvements +## [0.3.6] - 2026-01-05 -## [0.4.0] - Previous +### Changed +- Prepare for release with OpenCode plugin compatibility fixes -- Initial profiler support +## [0.3.5] - 2026-01-05 -## [0.3.0] - Previous +### Fixed +- Accept `PluginInput` parameter in plugin signature for OpenCode compatibility -- Added comprehensive dotenv parsing +## [0.3.4] - 2026-01-05 + +### Fixed +- Handle non-string input in `parseValue` function +- Add comprehensive tests for edge cases -## [0.2.0] - Previous +## [0.3.3] - 2026-01-05 +### Fixed +- Handle non-string input in `parseDotenv` to prevent runtime errors + +## [0.3.2] - 2026-01-05 + +### Fixed +- Handle non-string input in `expandPath` to prevent runtime crash + +## [0.3.1] - 2026-01-05 + +### Fixed +- Correct package URL in package.json + +## [0.3.0] - 2026-01-05 + +### Added +- Improved error handling throughout the plugin +- Better documentation + +### Changed +- More robust config file parsing + +## [0.2.0] - 2026-01-04 + +### Added - First public release +- Load `.env` files from configured paths +- Support for `~` home directory expansion +- JSONC config file support (`dotenv.jsonc`) +- Optional logging to file -## [0.1.0] - Previous +## [0.1.1] - 2026-01-04 +### Changed +- Prepare package for npm publish +- Add npm metadata + +## [0.1.0] - 2026-01-03 + +### Added - Initial implementation +- Basic dotenv file loading +- Plugin structure for OpenCode diff --git a/Makefile b/Makefile index 9c5000e..0baa6ee 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ .PHONY: build clean clean-all rebuild test typecheck bench .PHONY: publish publish-dry help .PHONY: release-tag release-push +.PHONY: wf-release wf-publish wf-ci wf-list wf-watch help: @echo "Targets:" @@ -13,8 +14,17 @@ help: @echo " make bench - Run benchmarks" @echo " make publish - Publish to npm" @echo " make publish-dry - Dry-run publish (preview)" + @echo "" + @echo "Release:" @echo " make release-tag - Create signed git tag (requires VERSION=X.Y.Z)" @echo " make release-push - Push commits and tags to remote" + @echo "" + @echo "Workflows (requires gh cli + fzf):" + @echo " make wf-release - Trigger release PR workflow (interactive)" + @echo " make wf-publish - Re-run failed publish workflow" + @echo " make wf-ci - Trigger CI workflow on current branch" + @echo " make wf-list - List recent workflow runs" + @echo " make wf-watch - Watch latest workflow run" # Build build: @@ -54,3 +64,41 @@ release-tag: release-push: git push --follow-tags + +# Workflows (requires gh cli + fzf) +# Trigger release PR workflow with interactive prompts +wf-release: + @TYPE=$$(echo -e "auto\npatch\nminor\nmajor" | fzf --prompt="Version type: " --height=6 --reverse); \ + if [ "$$TYPE" = "auto" ]; then \ + read -p "Custom version (leave empty for auto): " VERSION; \ + fi; \ + echo "→ Triggering release: type=$$TYPE version=$${VERSION:-auto}"; \ + gh workflow run release-pr.yml \ + -f version_type=$$TYPE \ + $${VERSION:+-f custom_version=$$VERSION}; \ + sleep 2; \ + $(MAKE) wf-watch W=release-pr.yml + +# Re-run failed publish workflow: make wf-publish [RUN=] +wf-publish: + $(if $(RUN),\ + gh run rerun $(RUN),\ + gh run rerun --failed -w release-publish.yml) + @sleep 2 + @$(MAKE) wf-watch W=release-publish.yml + +# Trigger CI workflow on current branch +wf-ci: + gh workflow run ci.yml --ref $(shell git branch --show-current) + @sleep 2 + @$(MAKE) wf-watch W=ci.yml + +# List recent workflow runs: make wf-list [W=] [N=10] +wf-list: + gh run list $(if $(W),-w $(W)) -L $(or $(N),10) + +# Watch latest workflow run: make wf-watch [W=] [RUN=] +wf-watch: + $(if $(RUN),\ + gh run watch $(RUN),\ + gh run watch $(shell gh run list $(if $(W),-w $(W)) -L 1 --json databaseId -q '.[0].databaseId')) diff --git a/README.md b/README.md index 09b624d..85bd962 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ # opencode-dotenv +[![npm version](https://badge.fury.io/js/opencode-dotenv.svg)](https://www.npmjs.com/package/opencode-dotenv) +[![npm downloads](https://img.shields.io/npm/dm/opencode-dotenv)](https://www.npmjs.com/package/opencode-dotenv) +[![license](https://img.shields.io/npm/l/opencode-dotenv)](LICENSE) +[![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue)](https://www.typescriptlang.org/) +[![Build Status](https://github.com/assagman/opencode-dotenv/actions/workflows/ci.yml/badge.svg)](https://github.com/assagman/opencode-dotenv/actions/workflows/ci.yml) +[![codecov](https://codecov.io/gh/assagman/opencode-dotenv/branch/main/graph/badge.svg)](https://codecov.io/gh/assagman/opencode-dotenv) + OpenCode plugin to load `.env` files at startup. > **Important Limitation** diff --git a/RELEASE.md b/RELEASE.md index 1019c38..96f85fb 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,136 +1,211 @@ # Release Process -This document describes the release process for opencode-dotenv. Follow these steps in order. +This document describes the automated release process for opencode-dotenv. -## Prerequisites +## Overview -- Git with GPG signing configured -- npm account with publish access -- GitHub CLI (`gh`) installed and authenticated +``` +┌──────────────────────────────────────────────────────────────────────────────┐ +│ RELEASE AUTOMATION FLOW │ +├──────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────────────┐ │ +│ │ 1. TRIGGER │ │ 2. REVIEW │ │ 3. AUTO-PUBLISH │ │ +│ │ │ │ │ │ (on PR merge) │ │ +│ │ Manual dispatch │───▶│ Release PR │───▶│ │ │ +│ │ via GitHub UI │ │ created │ │ • Create git tag │ │ +│ │ │ │ │ │ • GitHub release │ │ +│ │ Inputs: │ │ Branch: │ │ • npm publish │ │ +│ │ - version type │ │ release-vX.Y.Z │ │ │ │ +│ │ - custom ver │ │ │ │ │ │ +│ └─────────────────┘ └──────────────────┘ └─────────────────────────┘ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ +``` -## Release Steps +## Prerequisites -### 1. Determine Version Number +### One-time Setup -Review changes since last release and determine version bump: +1. **npm OIDC Trusted Publisher**: Configure via npm → Package Settings → Trusted Publishers + - Repository: `assagman/opencode-dotenv` + - Workflow: `release-publish.yml` + - Environment: `publish` + - **No secrets required** - uses OpenID Connect for secure, token-less publishing -```bash -git log --oneline $(git describe --tags --abbrev=0)..HEAD -``` +2. **GitHub Environment**: Create environment `publish` in repo settings + - Go to Settings → Environments → New environment → `publish` -Follow [Semantic Versioning](https://semver.org/): -- **MAJOR** (X.0.0): Breaking changes -- **MINOR** (0.X.0): New features, backward compatible -- **PATCH** (0.0.X): Bug fixes, backward compatible +3. **GitHub Labels**: Ensure these labels exist: + - `release` - Triggers the publish workflow on PR merge + - `automated` - Optional, for tracking automated PRs -### 2. Update CHANGELOG.md +4. **Codecov Token** (for CI): Add `CODECOV_TOKEN` secret for coverage uploads -Add a new section at the top of CHANGELOG.md (after the header): +## Creating a Release -```markdown -## [X.Y.Z] - YYYY-MM-DD +### Step 1: Trigger the Release Workflow -### Added -- New feature descriptions +**Option A: GitHub UI** +1. Go to **Actions** → **Create Release PR** +2. Click **Run workflow** +3. Select options: -### Changed -- Changes to existing functionality +| Option | Description | +|--------|-------------| +| `patch` | Bug fixes (0.0.X) | +| `minor` | New features, backward compatible (0.X.0) | +| `major` | Breaking changes (X.0.0) | +| `auto` | Analyze commits to determine version bump | +| Custom version | Override with specific version (e.g., `2.0.0`) | -### Fixed -- Bug fix descriptions +4. Click **Run workflow** -### Removed -- Removed features +**Option B: Command Line (requires fzf)** +```bash +make wf-release ``` -Use the commit history to write meaningful descriptions. Group related changes. +### Step 2: Review the Release PR -### 3. Bump Version in package.json +The workflow automatically: +- Creates branch `release-vX.Y.Z` from latest `main` +- Updates `package.json` version +- Updates `CHANGELOG.md` with categorized commits +- Creates a PR with the `release` label -Update the version field: +Review the PR: +- [ ] Verify version bump is correct +- [ ] Review and edit CHANGELOG if needed +- [ ] Ensure all CI checks pass -```json -"version": "X.Y.Z" -``` +### Step 3: Merge to Publish -### 4. Commit Release +When the PR is merged, the publish workflow automatically: +1. Creates a git tag `vX.Y.Z` +2. Creates a GitHub Release with auto-generated notes +3. Publishes to npm with provenance (via OIDC - no tokens needed) -Stage and commit both files together: +## Version Determination (Auto Mode) -```bash -git add CHANGELOG.md package.json -git commit -m "Release vX.Y.Z" -``` +When using `auto` version type, the workflow analyzes commits since the last tag: -### 5. Push to Remote +| Commit Pattern | Version Bump | +|----------------|--------------| +| `feat!:` or `BREAKING CHANGE` | **major** | +| `feat:` or `feature:` | **minor** | +| All other commits | **patch** | -```bash -git push -``` +Use [Conventional Commits](https://www.conventionalcommits.org/) for best results: +- `feat: add new feature` → minor +- `fix: resolve bug` → patch +- `feat!: breaking change` → major +- `chore: update deps` → patch -### 6. Create Signed Tag +## Manual Release (Fallback) -```bash -git tag -s -m "Release vX.Y.Z" vX.Y.Z -``` +If automation fails, follow this manual process: -### 7. Push Tag +### 1. Create Release Branch ```bash -git push --follow-tags +git checkout main +git pull origin main +git checkout -b release-vX.Y.Z ``` -### 8. Publish to npm +### 2. Update Version ```bash -npm publish +npm version X.Y.Z --no-git-tag-version ``` -> **Note**: This requires an OTP code from your authenticator app. The human must run this command. +### 3. Update CHANGELOG.md -### 9. Create GitHub Release +Add a new section: -Write release notes summarizing the changes (can be derived from CHANGELOG): +```markdown +## [X.Y.Z] - YYYY-MM-DD -```bash -gh release create vX.Y.Z --title "vX.Y.Z" --notes "RELEASE_NOTES_HERE" +### Added +- New features + +### Fixed +- Bug fixes ``` -For multi-line notes, use a heredoc: +### 4. Commit and Push ```bash -gh release create vX.Y.Z --title "vX.Y.Z" --notes "$(cat <<'EOF' -## What's New +git add package.json CHANGELOG.md +git commit -m "chore(release): prepare vX.Y.Z" +git push -u origin release-vX.Y.Z +``` -Summary of changes... +### 5. Create PR -### Features -- Feature 1 -- Feature 2 +```bash +gh pr create --title "Release vX.Y.Z" --label "release" --base main +``` -### Bug Fixes -- Fix 1 +### 6. After PR Merge (if auto-publish fails) -**Full Changelog**: https://github.com/assagman/opencode-dotenv/compare/vPREVIOUS...vX.Y.Z -EOF -)" +```bash +git checkout main +git pull +git tag -s -m "Release vX.Y.Z" vX.Y.Z +git push --tags +gh release create vX.Y.Z --title "vX.Y.Z" --generate-notes +npm publish ``` ## Verification After release, verify: -1. **npm**: https://www.npmjs.com/package/opencode-dotenv -2. **GitHub Releases**: https://github.com/assagman/opencode-dotenv/releases -3. **Git tags**: `git tag -l` +| Check | URL | +|-------|-----| +| npm package | https://www.npmjs.com/package/opencode-dotenv | +| GitHub Release | https://github.com/assagman/opencode-dotenv/releases | +| Git tags | `git tag -l` | -## Quick Reference (Make Targets) +## Troubleshooting -Helper targets for common operations: +### NPM Publish Fails -```bash -make release-tag VERSION=X.Y.Z # Create signed tag -make release-push # Push commits and tags -``` +- Verify `publish` environment exists in GitHub repo settings +- Check OIDC trusted publisher config matches workflow file name +- Ensure `id-token: write` permission is set in workflow +- Verify package name is available on npm + +### PR Not Triggering Publish + +- Verify PR has the `release` label +- Check PR was actually merged (not just closed) +- Review workflow run logs in Actions tab + +### Version Conflicts + +- If tag already exists, the workflow skips tag creation +- **Do not delete existing tags** - create a new patch version instead +- For problematic releases, deprecate the npm version: `npm deprecate opencode-dotenv@X.Y.Z "Reason"` + +## Workflow Files + +| File | Purpose | +|------|---------| +| `.github/workflows/ci.yml` | Runs tests and coverage on push/PR | +| `.github/workflows/release-pr.yml` | Creates release PR with version bump | +| `.github/workflows/release-publish.yml` | Publishes on PR merge | + +## Make Targets -> **Note**: `npm publish` requires OTP and must be run manually by the human. +| Target | Description | +|--------|-------------| +| `make wf-release` | Interactive release PR creation | +| `make wf-publish` | Re-run failed publish workflow | +| `make wf-ci` | Trigger CI on current branch | +| `make wf-list` | List recent workflow runs | +| `make wf-watch` | Watch latest workflow run | +| `make release-tag VERSION=X.Y.Z` | Create signed git tag | +| `make release-push` | Push commits and tags | diff --git a/package.json b/package.json index b5b322a..0b40b9b 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,14 @@ "name": "opencode-dotenv", "version": "0.5.1", "description": "OpenCode plugin to load .env files at startup", + "repository": { + "type": "git", + "url": "git+https://github.com/assagman/opencode-dotenv.git" + }, + "bugs": { + "url": "https://github.com/assagman/opencode-dotenv/issues" + }, + "homepage": "https://github.com/assagman/opencode-dotenv#readme", "main": "dist/index.js", "module": "dist/index.js", "type": "module",