From a452fe16e2c74341d5d399e247b833ddc272e97f Mon Sep 17 00:00:00 2001 From: James Davy Date: Fri, 21 Nov 2025 08:41:30 +0000 Subject: [PATCH 1/4] ci: tag and release --- .circleci/config.yml | 124 ++++++++++++++---- .github/workflows/pr-title-check.yml | 117 +++++++++++++++++ CONTRIBUTING.md | 53 ++++++++ docs/RELEASE.md | 182 +++++++++++++++++++++++++++ script/README.md | 23 +++- script/version-bump.sh | 47 +++++++ 6 files changed, 518 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/pr-title-check.yml create mode 100644 docs/RELEASE.md create mode 100755 script/version-bump.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index 0c5ec70..2295ad0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -67,43 +67,113 @@ jobs: mode: auto iac-scan: disabled + determine-version: + <<: *go_image + steps: + - checkout + - run: + name: Determine version bump and save to workspace + command: | + chmod +x ./script/version-bump.sh + ./script/version-bump.sh + + # Source the environment to make variables available + source $BASH_ENV + + # Save to workspace + mkdir -p /tmp/workspace + echo "$BUMP_TYPE" > /tmp/workspace/bump_type + echo "$NEW_VERSION" > /tmp/workspace/new_version + echo "$NEW_TAG" > /tmp/workspace/new_tag + echo "$PREVIOUS_TAG" > /tmp/workspace/previous_tag + - persist_to_workspace: + root: /tmp/workspace + paths: + - bump_type + - new_version + - new_tag + - previous_tag + + tag-release: + <<: *go_image + steps: + - checkout + - add_ssh_keys: + fingerprints: + - "SHA256:w5lYpE8DMWxUdasN8yMbbFdiz6s50PPBJMkV0a1iyZ8" + - attach_workspace: + at: /tmp/workspace + - run: + name: Configure git + command: | + git config user.email "ci@snyk.io" + git config user.name "Snyk CI" + - run: + name: Create and push tag + command: | + BUMP_TYPE=$(cat /tmp/workspace/bump_type) + NEW_TAG=$(cat /tmp/workspace/new_tag) + + if [ "$BUMP_TYPE" = "none" ]; then + echo "Chore commit detected - skipping tag creation" + circleci-agent step halt + fi + + echo "Creating tag: $NEW_TAG" + git tag -a "$NEW_TAG" -m "Release $NEW_TAG" + git push origin "$NEW_TAG" + +# Filters for branches +filters_pr_only: &filters_pr_only + filters: + branches: + ignore: + - main + +filters_main_only: &filters_main_only + filters: + branches: + only: + - main + workflows: version: 2 - CI: + test-and-tag: jobs: + # PR-only jobs - all testing + - lint: + <<: *filters_pr_only + + - unit_test: + <<: *filters_pr_only + - prodsec/secrets-scan: name: Scan repository for secrets context: - snyk-bot-slack channel: snyk-vuln-alerts-unify - filters: - branches: - ignore: - - main - - security-scans: - name: Security Scans - context: - - analysis_unify - - lint: - name: Lint - filters: - branches: - ignore: - - main - - unit_test: - name: Unit tests - filters: - branches: - ignore: - - main + <<: *filters_pr_only + - python_integration_test: - name: Python << matrix.python_version >> integration tests requires: - - Unit tests + - unit_test matrix: parameters: python_version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] - filters: - branches: - ignore: - - main + <<: *filters_pr_only + + # Main branch only - security scans and tagging (no test re-runs) + - security-scans: + context: + - analysis_unify + <<: *filters_main_only + + - determine-version: + requires: + - security-scans + <<: *filters_main_only + + - tag-release: + requires: + - determine-version + <<: *filters_main_only diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml new file mode 100644 index 0000000..9cd94e2 --- /dev/null +++ b/.github/workflows/pr-title-check.yml @@ -0,0 +1,117 @@ +name: PR Title Check + +on: + pull_request: + types: + - opened + - edited + - synchronize + - reopened + +jobs: + validate-pr-title: + name: Validate PR Title + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Validate PR Title Format + uses: actions/github-script@v7 + with: + script: | + const prTitle = context.payload.pull_request.title; + + // Define valid types and their version bump effect + const types = { + 'fix': 'patch', + 'feat': 'minor', + 'feature': 'minor', + 'chore': 'none', + 'docs': 'none', + 'style': 'none', + 'refactor': 'none', + 'test': 'none', + 'ci': 'none', + 'perf': 'patch', + 'build': 'none', + 'revert': 'patch' + }; + + // Regular expressions for validation + const conventionalCommitPattern = /^(\w+)(\(\w+\))?:\s.{3,}$/; + const majorPrefixPattern = /^\[major\]\s.{3,}$/i; + const majorTypePattern = /^major:\s.{3,}$/i; + + let isValid = false; + let bumpType = 'none'; + let errorMessage = ''; + + // Check for major version indicators + if (majorPrefixPattern.test(prTitle)) { + isValid = true; + bumpType = 'major'; + console.log('✓ Valid PR title with [major] prefix'); + } else if (majorTypePattern.test(prTitle)) { + isValid = true; + bumpType = 'major'; + console.log('✓ Valid PR title with major: prefix'); + } else if (conventionalCommitPattern.test(prTitle)) { + // Extract type from conventional commit format + const match = prTitle.match(/^(\w+)/); + const type = match ? match[1].toLowerCase() : ''; + + if (types[type]) { + isValid = true; + bumpType = types[type]; + console.log(`✓ Valid PR title with type '${type}' (${bumpType} bump)`); + } else { + errorMessage = `Invalid commit type '${type}'. Valid types are: ${Object.keys(types).join(', ')}`; + } + } else { + errorMessage = 'PR title does not follow conventional commit format'; + } + + // Helper to post/update comment + const marker = ''; + const postComment = async (body) => { + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + }); + const existing = comments.data.find(c => c.body.includes(marker)); + + const payload = { + owner: context.repo.owner, + repo: context.repo.repo, + body: `${marker}\n${body}` + }; + + if (existing) { + await github.rest.issues.updateComment({...payload, comment_id: existing.id}); + } else { + await github.rest.issues.createComment({...payload, issue_number: context.payload.pull_request.number}); + } + }; + + // Post result + if (isValid) { + const emoji = {'major': '🔴', 'minor': '🟡', 'patch': '🟢', 'none': '⚪'}[bumpType]; + const releaseNote = bumpType === 'none' ? ' (no release)' : ''; + await postComment(`✅ **Valid** — ${emoji} ${bumpType.toUpperCase()}${releaseNote}`); + console.log(`✓ Valid: ${bumpType} bump`); + } else { + const errorBody = [ + '❌ **Invalid**', + '', + `**Error:** ${errorMessage}`, + '', + '**Format:** `type: description` or `[major] description`', + '', + '**Valid types:** fix, feat, major, chore, docs, style, refactor, test, ci, perf, build, revert', + '', + 'See [CONTRIBUTING.md](https://github.com/snyk/cli-extension-dep-graph/blob/main/CONTRIBUTING.md) for examples.' + ].join('\n'); + await postComment(errorBody); + core.setFailed(errorMessage); + } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 95e6ade..37f4e59 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,3 +3,56 @@ This repo is intended for internal (Snyk) contributions only at this time. Please [reach our support](SUPPORT.md) to give any feedback. + +## Commit Message Convention + +This project uses **Conventional Commits** for automated versioning and releases. Please follow this format when committing or creating pull requests: + +### Format +``` +[optional scope]: + +[optional body] + +[optional footer(s)] +``` + +### Types and Their Effect on Versioning + +- **`fix:`** - Bug fixes (bumps **PATCH** version: 1.0.0 → 1.0.1) +- **`feat:`** - New features (bumps **MINOR** version: 1.0.0 → 1.1.0) +- **`[major]`** or **`major:`** - Breaking changes (bumps **MAJOR** version: 1.0.0 → 2.0.0) +- **`chore:`**, **`docs:`**, **`style:`**, **`refactor:`**, **`test:`**, **`ci:`** - No release created + +### Examples + +```bash +# Patch release +fix: resolve memory leak in dependency parser + +# Minor release +feat: add support for Python 3.13 + +# Major release (option 1) +[major] redesign API interface + +# Major release (option 2) +major: remove deprecated functions + +# No release +chore: update CI configuration +``` + +### Pull Request Guidelines + +When creating a pull request: +1. **Use a descriptive title** following the conventional commit format (examples above) +2. **Check the GitHub Action** - A bot will automatically validate your PR title and post a comment showing: + - Whether the title is valid ✅ or invalid ❌ + - The version bump type (MAJOR, MINOR, PATCH, or NO RELEASE) +3. **Edit the title if needed** - If validation fails or shows the wrong version bump, edit your PR title +4. **Use "Squash and merge"** - The PR title will become the commit message +5. **Ensure all tests pass** before merging +6. **Releases are automatic** - When merged to `main`, CircleCI will automatically create a release + +For more details, see the [Release Process Documentation](docs/RELEASE.md). diff --git a/docs/RELEASE.md b/docs/RELEASE.md new file mode 100644 index 0000000..2ad6e95 --- /dev/null +++ b/docs/RELEASE.md @@ -0,0 +1,182 @@ +# Release Process + +This document describes the automated release process for the `cli-extension-dep-graph` project. + +## Overview + +The project uses **Semantic Versioning** (semver) and automates releases using CircleCI and GoReleaser. Releases are triggered automatically when code is merged to the `main` branch. + +The version bump is determined by the **Pull Request title**, which must follow the Conventional Commits format. A GitHub Action automatically validates PR titles when you create or edit a PR. + +## How It Works + +### 1. PR Title Convention + +The release system uses **Pull Request titles** following the **Conventional Commits** format to determine version bumps: + +- **`fix:`** or **`fix(scope):`** → Bumps **PATCH** version (e.g., 1.0.0 → 1.0.1) +- **`feat:`** or **`feat(scope):`** → Bumps **MINOR** version (e.g., 1.0.0 → 1.1.0) +- **`[major]`** or **`major:`** → Bumps **MAJOR** version (e.g., 1.0.0 → 2.0.0) +- **`chore:`**, **`docs:`**, **`style:`**, **`refactor:`**, **`test:`**, **`ci:`** → **No release** created + +### 2. PR Title Examples + +When creating a Pull Request, use these title formats: + +``` +# Patch version bump (bug fixes) +fix: resolve null pointer exception in parser +fix(parser): handle edge case in dependency resolution + +# Minor version bump (new features) +feat: add support for new dependency format +feat(python): add Python 3.13 support + +# Major version bump (breaking changes) - Option 1 +[major] redesign API interface + +# Major version bump (breaking changes) - Option 2 +major: remove deprecated functions + +# No release (maintenance) +chore: update dependencies +docs: improve README documentation +test: add integration tests for edge cases +``` + +### 3. GitHub Action - PR Title Validation + +When you create or edit a Pull Request: + +1. A GitHub Action automatically validates the PR title +2. The action posts a comment showing: + - ✅ Whether the title is valid + - The version bump type (MAJOR, MINOR, PATCH, or NO RELEASE) + - Examples if the title is invalid +3. The PR cannot be merged if the title validation fails + +### 4. CircleCI Workflow + +When code is merged to `main` using **"Squash and merge"**, the following happens: + +1. **Run Tests**: All tests (lint, unit tests, integration tests, security scans) must pass +2. **Determine Version**: The `version-bump.sh` script reads the commit subject (PR title) and determines the version bump +3. **Tag Release**: If a release is needed (not a chore), a git tag is created and pushed (e.g., `v1.2.3`) +4. **Build & Release**: GoReleaser creates: + - GitHub Release with changelog + - Source archives for Linux, macOS, and Windows + - Checksums for verification + +**Note:** Always use "Squash and merge" when merging PRs to ensure the PR title becomes the commit message. + +### 5. Increasing the Major Version + +There are two ways to trigger a **major version bump** in your PR title: + +#### Option A: Use `[major]` prefix +``` +PR Title: [major] redesign core architecture +``` + +#### Option B: Use `major:` prefix +``` +PR Title: major: remove support for deprecated features +``` + +**Note:** The `BREAKING CHANGE` keyword in the PR description/body is not currently supported for major version bumps. You must use `[major]` or `major:` in the PR title. + +## Manual Release (Local Testing) + +To test the release process locally: + +```bash +# 1. Ensure you have GoReleaser installed +brew install goreleaser + +# 2. Test the version bump script +./script/version-bump.sh + +# 3. Test GoReleaser (snapshot mode, won't publish) +goreleaser release --snapshot --clean + +# 4. Check the generated artifacts in ./dist/ +ls -la ./dist/ +``` + +## CircleCI Configuration + +### Required SSH Key + +The CircleCI project must have an SSH key configured for pushing tags to GitHub: + +1. Go to CircleCI → Project Settings → SSH Keys +2. Add a new SSH key with write access to the repository +3. The fingerprint is already configured in `.circleci/config.yml`: + - `SHA256:w5lYpE8DMWxUdasN8yMbbFdiz6s50PPBJMkV0a1iyZ8` + +**Note:** The SSH key must have push permissions to the `snyk/cli-extension-dep-graph` repository. + +## Release Artifacts + +Each release includes: + +- **Source archives**: `.tar.gz` and `.zip` formats +- **Checksums**: SHA256 checksums in `checksums.txt` +- **Changelog**: Auto-generated from commit messages +- **GitHub Release**: Published at https://github.com/snyk/cli-extension-dep-graph/releases + +## Troubleshooting + +### Release not created + +**Problem**: Code was merged to `main` but no release was created. + +**Solutions**: +- Check if PR title starts with `chore:`, `docs:`, etc. (these skip releases) +- Verify all tests passed in CircleCI +- Check CircleCI logs for the "Determine Version" job +- Verify the PR title was correctly formatted + +### Wrong version bump + +**Problem**: Expected a minor bump but got a patch bump. + +**Solutions**: +- Verify PR title follows convention: `feat:` for minor, `fix:` for patch +- Check the "Determine Version" job output in CircleCI +- PR title must match the regex patterns in `version-bump.sh` +- Look at the GitHub Action comment on the PR to see what version bump was expected + +### Tag push fails with authentication error + +**Problem**: Tag creation fails with permission denied or authentication error. + +**Solutions**: +- Verify SSH key is configured in CircleCI project settings +- Ensure the fingerprint in `.circleci/config.yml` matches the key in CircleCI +- Check that the SSH key has push permissions to the repository +- Verify the SSH key hasn't been revoked or removed from GitHub + +### Tag already exists + +**Problem**: Tag creation fails because tag already exists. + +**Solutions**: +- Delete the existing tag: `git tag -d v1.2.3 && git push origin :refs/tags/v1.2.3` +- Re-run the failed CircleCI workflow + +## Best Practices + +1. **Use Conventional Commits**: Always format PR titles using the conventional commit format for clear version bumps and changelogs +2. **Validate PR Title**: The GitHub Action will validate your PR title automatically and show the expected version bump +3. **Squash and Merge**: When merging PRs, use "Squash and merge" - the PR title will become the commit message +4. **Update PR Title if Needed**: If the GitHub Action shows an invalid title or wrong version bump, edit the PR title before merging +5. **Test Before Merging**: Ensure all tests pass before merging to `main` +6. **Review Changelog**: After release, review the auto-generated changelog for accuracy + +## References + +- [Conventional Commits](https://www.conventionalcommits.org/) +- [Semantic Versioning](https://semver.org/) +- [GoReleaser Documentation](https://goreleaser.com/) +- [CircleCI Workflows](https://circleci.com/docs/workflows/) diff --git a/script/README.md b/script/README.md index ec86342..8bdf856 100644 --- a/script/README.md +++ b/script/README.md @@ -2,4 +2,25 @@ Scripts to perform various build, install, analysis, etc operations. -These scripts keep the root level Makefile small and simple. \ No newline at end of file +These scripts keep the root level Makefile small and simple. + +## Available Scripts + +### `lint.sh` +Runs linting checks on the codebase using golangci-lint. + +### `version-bump.sh` +Reads the commit subject (PR title) and determines the semantic version bump. Used by CircleCI for automated releases. + +**Usage:** +```bash +./script/version-bump.sh +``` + +**Supported formats:** +- `fix:`, `perf:`, `revert:` → PATCH bump +- `feat:`, `feature:` → MINOR bump +- `[major]`, `major:` → MAJOR bump +- `chore:`, `docs:`, `test:`, `ci:`, `style:`, `refactor:`, `build:` → No release + +Requires "Squash and merge" to ensure PR title becomes the commit message. \ No newline at end of file diff --git a/script/version-bump.sh b/script/version-bump.sh new file mode 100755 index 0000000..3e168ad --- /dev/null +++ b/script/version-bump.sh @@ -0,0 +1,47 @@ +#!/bin/bash +set -e + +# Get latest tag or default to v0.0.0 +LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") +VERSION=${LATEST_TAG#v} + +# Parse version components +IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION" + +# Get PR title (commit subject line) +PR_TITLE=$(git log -1 --pretty=%s) +echo "PR title: $PR_TITLE" + +# Determine version bump based on PR title +if [[ "$PR_TITLE" =~ ^\[major\]|^major: ]]; then + BUMP_TYPE="major" + MAJOR=$((MAJOR + 1)) + MINOR=0 + PATCH=0 +elif [[ "$PR_TITLE" =~ ^feat(\(.*\))?:|^feature: ]]; then + BUMP_TYPE="minor" + MINOR=$((MINOR + 1)) + PATCH=0 +elif [[ "$PR_TITLE" =~ ^(fix|perf|revert)(\(.*\))?: ]]; then + BUMP_TYPE="patch" + PATCH=$((PATCH + 1)) +elif [[ "$PR_TITLE" =~ ^(chore|docs|style|refactor|test|ci|build)(\(.*\))?: ]]; then + BUMP_TYPE="none" +else + echo "Warning: No recognized commit type, defaulting to PATCH" + BUMP_TYPE="patch" + PATCH=$((PATCH + 1)) +fi + +NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}" +NEW_TAG="v${NEW_VERSION}" + +echo "Bump type: $BUMP_TYPE ($LATEST_TAG → $NEW_TAG)" + +# Export for CircleCI (only if BASH_ENV is set) +if [ -n "$BASH_ENV" ]; then + echo "export BUMP_TYPE=$BUMP_TYPE" >> $BASH_ENV + echo "export NEW_VERSION=$NEW_VERSION" >> $BASH_ENV + echo "export NEW_TAG=$NEW_TAG" >> $BASH_ENV + echo "export PREVIOUS_TAG=$LATEST_TAG" >> $BASH_ENV +fi From c9850216d929755f0c888fe25a8f2c3ad79daafe Mon Sep 17 00:00:00 2001 From: James Davy Date: Mon, 24 Nov 2025 10:43:53 +0000 Subject: [PATCH 2/4] use pr-conventional-commits git action --- .github/workflows/pr-title-check.yml | 102 +-------------------------- CONTRIBUTING.md | 23 +++--- docs/RELEASE.md | 50 ++++++------- script/README.md | 2 +- script/version-bump.sh | 9 +-- 5 files changed, 47 insertions(+), 139 deletions(-) diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml index 9cd94e2..e1b86f1 100644 --- a/.github/workflows/pr-title-check.yml +++ b/.github/workflows/pr-title-check.yml @@ -15,103 +15,7 @@ jobs: permissions: pull-requests: write steps: - - name: Validate PR Title Format - uses: actions/github-script@v7 + - name: Conventional Commit In Pull Requests + uses: ytanikin/pr-conventional-commits@1.4.2 with: - script: | - const prTitle = context.payload.pull_request.title; - - // Define valid types and their version bump effect - const types = { - 'fix': 'patch', - 'feat': 'minor', - 'feature': 'minor', - 'chore': 'none', - 'docs': 'none', - 'style': 'none', - 'refactor': 'none', - 'test': 'none', - 'ci': 'none', - 'perf': 'patch', - 'build': 'none', - 'revert': 'patch' - }; - - // Regular expressions for validation - const conventionalCommitPattern = /^(\w+)(\(\w+\))?:\s.{3,}$/; - const majorPrefixPattern = /^\[major\]\s.{3,}$/i; - const majorTypePattern = /^major:\s.{3,}$/i; - - let isValid = false; - let bumpType = 'none'; - let errorMessage = ''; - - // Check for major version indicators - if (majorPrefixPattern.test(prTitle)) { - isValid = true; - bumpType = 'major'; - console.log('✓ Valid PR title with [major] prefix'); - } else if (majorTypePattern.test(prTitle)) { - isValid = true; - bumpType = 'major'; - console.log('✓ Valid PR title with major: prefix'); - } else if (conventionalCommitPattern.test(prTitle)) { - // Extract type from conventional commit format - const match = prTitle.match(/^(\w+)/); - const type = match ? match[1].toLowerCase() : ''; - - if (types[type]) { - isValid = true; - bumpType = types[type]; - console.log(`✓ Valid PR title with type '${type}' (${bumpType} bump)`); - } else { - errorMessage = `Invalid commit type '${type}'. Valid types are: ${Object.keys(types).join(', ')}`; - } - } else { - errorMessage = 'PR title does not follow conventional commit format'; - } - - // Helper to post/update comment - const marker = ''; - const postComment = async (body) => { - const comments = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.pull_request.number, - }); - const existing = comments.data.find(c => c.body.includes(marker)); - - const payload = { - owner: context.repo.owner, - repo: context.repo.repo, - body: `${marker}\n${body}` - }; - - if (existing) { - await github.rest.issues.updateComment({...payload, comment_id: existing.id}); - } else { - await github.rest.issues.createComment({...payload, issue_number: context.payload.pull_request.number}); - } - }; - - // Post result - if (isValid) { - const emoji = {'major': '🔴', 'minor': '🟡', 'patch': '🟢', 'none': '⚪'}[bumpType]; - const releaseNote = bumpType === 'none' ? ' (no release)' : ''; - await postComment(`✅ **Valid** — ${emoji} ${bumpType.toUpperCase()}${releaseNote}`); - console.log(`✓ Valid: ${bumpType} bump`); - } else { - const errorBody = [ - '❌ **Invalid**', - '', - `**Error:** ${errorMessage}`, - '', - '**Format:** `type: description` or `[major] description`', - '', - '**Valid types:** fix, feat, major, chore, docs, style, refactor, test, ci, perf, build, revert', - '', - 'See [CONTRIBUTING.md](https://github.com/snyk/cli-extension-dep-graph/blob/main/CONTRIBUTING.md) for examples.' - ].join('\n'); - await postComment(errorBody); - core.setFailed(errorMessage); - } + task_types: '["feat","fix","docs","style","refactor","perf","test","build","ci","chore","revert"]' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 37f4e59..d5538a0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,7 +21,7 @@ This project uses **Conventional Commits** for automated versioning and releases - **`fix:`** - Bug fixes (bumps **PATCH** version: 1.0.0 → 1.0.1) - **`feat:`** - New features (bumps **MINOR** version: 1.0.0 → 1.1.0) -- **`[major]`** or **`major:`** - Breaking changes (bumps **MAJOR** version: 1.0.0 → 2.0.0) +- **`type!:`** - Breaking changes (bumps **MAJOR** version: 1.0.0 → 2.0.0) - **`chore:`**, **`docs:`**, **`style:`**, **`refactor:`**, **`test:`**, **`ci:`** - No release created ### Examples @@ -29,28 +29,31 @@ This project uses **Conventional Commits** for automated versioning and releases ```bash # Patch release fix: resolve memory leak in dependency parser +fix(parser): handle edge case in requirements file # Minor release feat: add support for Python 3.13 +feat(python): add environment marker support -# Major release (option 1) -[major] redesign API interface - -# Major release (option 2) -major: remove deprecated functions +# Major release (breaking changes - use ! indicator) +fix!: change API return type +feat(api)!: redesign core interface +refactor!: remove deprecated parser +chore!: drop support for Python 3.7 # No release chore: update CI configuration +docs: improve README documentation ``` ### Pull Request Guidelines When creating a pull request: 1. **Use a descriptive title** following the conventional commit format (examples above) -2. **Check the GitHub Action** - A bot will automatically validate your PR title and post a comment showing: - - Whether the title is valid ✅ or invalid ❌ - - The version bump type (MAJOR, MINOR, PATCH, or NO RELEASE) -3. **Edit the title if needed** - If validation fails or shows the wrong version bump, edit your PR title +2. **Check the GitHub Action** - A check will automatically validate your PR title format + - ✅ Check passes if title is valid (e.g., `feat:`, `fix:`, `fix!:`) + - ❌ Check fails if title format is invalid +3. **Edit the title if needed** - If validation fails, edit your PR title before merging 4. **Use "Squash and merge"** - The PR title will become the commit message 5. **Ensure all tests pass** before merging 6. **Releases are automatic** - When merged to `main`, CircleCI will automatically create a release diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 2ad6e95..eb4e31a 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -16,7 +16,7 @@ The release system uses **Pull Request titles** following the **Conventional Com - **`fix:`** or **`fix(scope):`** → Bumps **PATCH** version (e.g., 1.0.0 → 1.0.1) - **`feat:`** or **`feat(scope):`** → Bumps **MINOR** version (e.g., 1.0.0 → 1.1.0) -- **`[major]`** or **`major:`** → Bumps **MAJOR** version (e.g., 1.0.0 → 2.0.0) +- **`type!:`** (breaking change) → Bumps **MAJOR** version (e.g., 1.0.0 → 2.0.0) - **`chore:`**, **`docs:`**, **`style:`**, **`refactor:`**, **`test:`**, **`ci:`** → **No release** created ### 2. PR Title Examples @@ -32,11 +32,12 @@ fix(parser): handle edge case in dependency resolution feat: add support for new dependency format feat(python): add Python 3.13 support -# Major version bump (breaking changes) - Option 1 -[major] redesign API interface - -# Major version bump (breaking changes) - Option 2 -major: remove deprecated functions +# Major version bump (breaking changes) +# Use ! after type or type(scope) to indicate breaking changes +fix!: change return type of core API +feat(api)!: redesign authentication flow +refactor(parser)!: remove support for legacy format +chore!: drop support for Python 3.7 # No release (maintenance) chore: update dependencies @@ -48,12 +49,11 @@ test: add integration tests for edge cases When you create or edit a Pull Request: -1. A GitHub Action automatically validates the PR title -2. The action posts a comment showing: - - ✅ Whether the title is valid - - The version bump type (MAJOR, MINOR, PATCH, or NO RELEASE) - - Examples if the title is invalid -3. The PR cannot be merged if the title validation fails +1. A GitHub Action automatically validates the PR title against the Conventional Commits format +2. The action will: + - ✅ Pass if the title follows the correct format (e.g., `feat:`, `fix:`, `fix!:`) + - ❌ Fail if the title is invalid +3. The check must pass before the PR can be merged ### 4. CircleCI Workflow @@ -71,19 +71,19 @@ When code is merged to `main` using **"Squash and merge"**, the following happen ### 5. Increasing the Major Version -There are two ways to trigger a **major version bump** in your PR title: +To trigger a **major version bump**, add `!` after the type (or scope) in your PR title: -#### Option A: Use `[major]` prefix ``` -PR Title: [major] redesign core architecture +# Breaking change examples +PR Title: fix!: change API return type +PR Title: feat(api)!: redesign authentication flow +PR Title: refactor(parser)!: remove legacy format support +PR Title: chore!: drop support for Python 3.7 ``` -#### Option B: Use `major:` prefix -``` -PR Title: major: remove support for deprecated features -``` +**Standard:** This follows the [Conventional Commits specification](https://www.conventionalcommits.org/) which uses `!` to indicate breaking changes in the commit subject line. -**Note:** The `BREAKING CHANGE` keyword in the PR description/body is not currently supported for major version bumps. You must use `[major]` or `major:` in the PR title. +**Note:** The `BREAKING CHANGE:` footer in commit bodies is part of the Conventional Commits spec, but our PR title validation only checks the subject line. Use `!` in the PR title for automatic major version bumps. ## Manual Release (Local Testing) @@ -142,10 +142,10 @@ Each release includes: **Problem**: Expected a minor bump but got a patch bump. **Solutions**: -- Verify PR title follows convention: `feat:` for minor, `fix:` for patch -- Check the "Determine Version" job output in CircleCI +- Verify PR title follows convention: `feat:` for minor, `fix:` for patch, `type!:` for major +- Check the "Determine Version" job output in CircleCI to see the calculated version bump - PR title must match the regex patterns in `version-bump.sh` -- Look at the GitHub Action comment on the PR to see what version bump was expected +- Ensure the GitHub Action validation check passed on the PR ### Tag push fails with authentication error @@ -168,9 +168,9 @@ Each release includes: ## Best Practices 1. **Use Conventional Commits**: Always format PR titles using the conventional commit format for clear version bumps and changelogs -2. **Validate PR Title**: The GitHub Action will validate your PR title automatically and show the expected version bump +2. **Validate PR Title**: The GitHub Action will validate your PR title format automatically 3. **Squash and Merge**: When merging PRs, use "Squash and merge" - the PR title will become the commit message -4. **Update PR Title if Needed**: If the GitHub Action shows an invalid title or wrong version bump, edit the PR title before merging +4. **Update PR Title if Needed**: If the GitHub Action check fails, edit the PR title before merging 5. **Test Before Merging**: Ensure all tests pass before merging to `main` 6. **Review Changelog**: After release, review the auto-generated changelog for accuracy diff --git a/script/README.md b/script/README.md index 8bdf856..09cf7bc 100644 --- a/script/README.md +++ b/script/README.md @@ -20,7 +20,7 @@ Reads the commit subject (PR title) and determines the semantic version bump. Us **Supported formats:** - `fix:`, `perf:`, `revert:` → PATCH bump - `feat:`, `feature:` → MINOR bump -- `[major]`, `major:` → MAJOR bump +- `type!:` (e.g., `fix!:`, `feat(api)!:`) → MAJOR bump (breaking changes) - `chore:`, `docs:`, `test:`, `ci:`, `style:`, `refactor:`, `build:` → No release Requires "Squash and merge" to ensure PR title becomes the commit message. \ No newline at end of file diff --git a/script/version-bump.sh b/script/version-bump.sh index 3e168ad..2749295 100755 --- a/script/version-bump.sh +++ b/script/version-bump.sh @@ -13,7 +13,8 @@ PR_TITLE=$(git log -1 --pretty=%s) echo "PR title: $PR_TITLE" # Determine version bump based on PR title -if [[ "$PR_TITLE" =~ ^\[major\]|^major: ]]; then +if [[ "$PR_TITLE" =~ ^[a-z]+(\(.*\))?!: ]]; then + # Breaking change indicator with ! (e.g., fix!:, feat(api)!:) BUMP_TYPE="major" MAJOR=$((MAJOR + 1)) MINOR=0 @@ -28,9 +29,9 @@ elif [[ "$PR_TITLE" =~ ^(fix|perf|revert)(\(.*\))?: ]]; then elif [[ "$PR_TITLE" =~ ^(chore|docs|style|refactor|test|ci|build)(\(.*\))?: ]]; then BUMP_TYPE="none" else - echo "Warning: No recognized commit type, defaulting to PATCH" - BUMP_TYPE="patch" - PATCH=$((PATCH + 1)) + echo "Warning: No recognized commit type, defaulting to NONE (no release)" + echo "PR title should follow Conventional Commits format (validated by GitHub Action)" + BUMP_TYPE="none" fi NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}" From 026687b65eca99e43401c53ab9a82311ac0523e5 Mon Sep 17 00:00:00 2001 From: James Davy Date: Mon, 24 Nov 2025 11:27:57 +0000 Subject: [PATCH 3/4] use circleci context --- .circleci/config.yml | 9 +++++---- docs/RELEASE.md | 33 ++++++++++++++++++++++----------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 2295ad0..d244a63 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -98,9 +98,6 @@ jobs: <<: *go_image steps: - checkout - - add_ssh_keys: - fingerprints: - - "SHA256:w5lYpE8DMWxUdasN8yMbbFdiz6s50PPBJMkV0a1iyZ8" - attach_workspace: at: /tmp/workspace - run: @@ -121,7 +118,9 @@ jobs: echo "Creating tag: $NEW_TAG" git tag -a "$NEW_TAG" -m "Release $NEW_TAG" - git push origin "$NEW_TAG" + + # Push using HTTPS with GitHub token (no fingerprint needed) + git push https://${GH_TOKEN}@github.com/snyk/cli-extension-dep-graph.git "$NEW_TAG" # Filters for branches filters_pr_only: &filters_pr_only @@ -174,6 +173,8 @@ workflows: <<: *filters_main_only - tag-release: + context: + - os-ecosystems requires: - determine-version <<: *filters_main_only diff --git a/docs/RELEASE.md b/docs/RELEASE.md index eb4e31a..0e85584 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -105,16 +105,26 @@ ls -la ./dist/ ## CircleCI Configuration -### Required SSH Key +### Required Context Variable -The CircleCI project must have an SSH key configured for pushing tags to GitHub: +The CircleCI `os-ecosystems` context must contain: -1. Go to CircleCI → Project Settings → SSH Keys -2. Add a new SSH key with write access to the repository -3. The fingerprint is already configured in `.circleci/config.yml`: - - `SHA256:w5lYpE8DMWxUdasN8yMbbFdiz6s50PPBJMkV0a1iyZ8` +- **`GH_TOKEN`**: GitHub Personal Access Token or GitHub App token with `repo` permissions for pushing tags -**Note:** The SSH key must have push permissions to the `snyk/cli-extension-dep-graph` repository. +### Context Setup + +1. Go to CircleCI → Organization Settings → Contexts +2. Use existing context: `os-ecosystems` +3. Ensure the context has the environment variable: + - **Name:** `GH_TOKEN` + - **Value:** GitHub token with `repo` scope + - Create token at: https://github.com/settings/tokens (if needed) + +**Benefits:** +- ✅ No SSH key fingerprints to manage +- ✅ Easy to rotate - just update the context variable +- ✅ No risk of broken fingerprints after key rotation +- ✅ Works immediately without SSH key setup ## Release Artifacts @@ -152,10 +162,11 @@ Each release includes: **Problem**: Tag creation fails with permission denied or authentication error. **Solutions**: -- Verify SSH key is configured in CircleCI project settings -- Ensure the fingerprint in `.circleci/config.yml` matches the key in CircleCI -- Check that the SSH key has push permissions to the repository -- Verify the SSH key hasn't been revoked or removed from GitHub +- Verify `GH_TOKEN` is set in the CircleCI context: `os-ecosystems` +- Ensure the token has `repo` scope permissions +- Check that the token hasn't expired +- Verify the token is from a user/app with push permissions to the repository +- Test the token manually: `curl -H "Authorization: token $GH_TOKEN" https://api.github.com/user` ### Tag already exists From 8e9c05dd95543b99d52f0a64ec5d15a9f2796779 Mon Sep 17 00:00:00 2001 From: James Davy Date: Mon, 24 Nov 2025 11:45:52 +0000 Subject: [PATCH 4/4] use circleci env vars --- .circleci/config.yml | 2 +- docs/RELEASE.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d244a63..331291d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -120,7 +120,7 @@ jobs: git tag -a "$NEW_TAG" -m "Release $NEW_TAG" # Push using HTTPS with GitHub token (no fingerprint needed) - git push https://${GH_TOKEN}@github.com/snyk/cli-extension-dep-graph.git "$NEW_TAG" + git push https://${GH_TOKEN}@github.com/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME} "$NEW_TAG" # Filters for branches filters_pr_only: &filters_pr_only diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 0e85584..561eb32 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -61,7 +61,7 @@ When code is merged to `main` using **"Squash and merge"**, the following happen 1. **Run Tests**: All tests (lint, unit tests, integration tests, security scans) must pass 2. **Determine Version**: The `version-bump.sh` script reads the commit subject (PR title) and determines the version bump -3. **Tag Release**: If a release is needed (not a chore), a git tag is created and pushed (e.g., `v1.2.3`) +3. **Tag Release**: If a release is needed (not a chore), a git tag is created and pushed (e.g., `v1.2.3`) using CircleCI's built-in environment variables (`CIRCLE_PROJECT_USERNAME` and `CIRCLE_PROJECT_REPONAME`) 4. **Build & Release**: GoReleaser creates: - GitHub Release with changelog - Source archives for Linux, macOS, and Windows