Skip to content
Open
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
112 changes: 112 additions & 0 deletions .github/workflows/changelog.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
name: Update Changelog

on:
push:
branches: [main]
paths-ignore:
- 'CHANGELOG.md'
- 'docs/**'
- '**.md'
Comment on lines +6 to +9
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

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

The paths-ignore configuration will exclude all markdown files from triggering the workflow. This means that changes to non-documentation markdown files (if any exist) will also be ignored. More critically, this creates a potential infinite loop: if the workflow commits to CHANGELOG.md and the push includes other non-markdown code changes, it could trigger itself again. Consider a more specific approach or ensure the workflow checks for the commit author to avoid loops.

Copilot uses AI. Check for mistakes.

permissions:
contents: write
pull-requests: write

jobs:
update-changelog:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Need full history for changelog generation

- name: Get last tag
id: last_tag
run: |
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -z "$LAST_TAG" ]; then
echo "No previous tag found, using first commit"
LAST_TAG=$(git rev-list --max-parents=0 HEAD)
fi
echo "tag=$LAST_TAG" >> $GITHUB_OUTPUT
echo "Last tag: $LAST_TAG"

- name: Generate changelog for unreleased changes
id: changelog
run: |
if [ ! -f scripts/generate-changelog ]; then
echo "Changelog script not found, skipping"
echo "skip=true" >> $GITHUB_OUTPUT
exit 0
fi

# Generate changelog from last tag to HEAD
CHANGELOG=$(bash scripts/generate-changelog "${{ steps.last_tag.outputs.tag }}" "HEAD" || echo "")

if [ -z "$CHANGELOG" ]; then
echo "No new changes since last tag"
echo "skip=true" >> $GITHUB_OUTPUT
exit 0
fi

# Save to file
echo "$CHANGELOG" > /tmp/changelog-unreleased.md
echo "skip=false" >> $GITHUB_OUTPUT

- name: Update CHANGELOG.md
if: steps.changelog.outputs.skip != 'true'
run: |
# Check if [Unreleased] section exists
if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
echo "[Unreleased] section not found in CHANGELOG.md"
exit 0
fi

# Create backup
cp CHANGELOG.md CHANGELOG.md.bak

# Replace [Unreleased] section with new content
{
# Everything before [Unreleased]
sed -n '1,/## \[Unreleased\]/p' CHANGELOG.md | head -n -1

# New changelog content
cat /tmp/changelog-unreleased.md

# Everything after [Unreleased] section
sed -n '/## \[Unreleased\]/,$ {
/## \[Unreleased\]/d
/^$/d
p
}' CHANGELOG.md | sed '1,/^## \[/d' | sed '1s/^/\n/'
} > CHANGELOG.md.tmp

# Check if there are actual changes
if diff -q CHANGELOG.md CHANGELOG.md.bak >/dev/null 2>&1; then
echo "No changes to CHANGELOG.md"
rm CHANGELOG.md.tmp CHANGELOG.md.bak
exit 0
fi

mv CHANGELOG.md.tmp CHANGELOG.md
rm CHANGELOG.md.bak

- name: Commit changes
if: steps.changelog.outputs.skip != 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

if git diff --quiet CHANGELOG.md; then
echo "No changes to commit"
exit 0
fi

git add CHANGELOG.md
git commit -m "docs: Update unreleased changes in CHANGELOG.md

Automatically generated from recent commits

🤖 Generated by GitHub Actions"

git push
45 changes: 45 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,56 @@ When contributing, please consider:

### Commit Messages

We use [Conventional Commits](https://www.conventionalcommits.org/) for automatic changelog generation:

```
<type>: <description>

[optional body]

[optional footer]
```

**Types:**

- `feat:` - New feature (appears in changelog under "Added")
- `fix:` - Bug fix (appears in changelog under "Fixed")
- `perf:` - Performance improvement (appears in changelog under "Performance")
- `docs:` - Documentation changes (appears in changelog under "Documentation")
- `refactor:` - Code refactoring (appears in changelog under "Changed")
- `test:` - Test changes (appears in changelog under "Tests")
- `chore:` - Maintenance tasks (appears in changelog under "Maintenance")
- `ci:` - CI/CD changes (appears in changelog under "CI/CD")

**Examples:**

```bash
# Feature
git commit -m "feat: Add dark mode support"

# Bug fix
git commit -m "fix: Resolve certificate expiration notification timing"

# Breaking change
git commit -m "feat!: Change config format to TOML

BREAKING CHANGE: Config format changed from JSON to TOML."

# With issue reference
git commit -m "fix: Handle SSH username extraction from clone URLs

Fixes #42"
```

**Additional guidelines:**

- Use the present tense ("Add feature" not "Added feature")
- Use the imperative mood ("Move cursor to..." not "Moves cursor to...")
- Limit the first line to 72 characters
- Reference issues and PRs in the body when relevant

See [docs/releases.md](docs/releases.md) for more on the release process.

### Documentation

- Update `README.md` for user-facing changes
Expand Down
47 changes: 46 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ POLICY_FILE ?= cassh.policy.example.toml
icon app-bundle app-bundle-oss app-bundle-enterprise \
dmg dmg-only pkg pkg-only \
sign notarize \
test lint
test test-race test-coverage test-ci test-list \
test-ca test-config test-memes test-menubar \
lint \
changelog changelog-preview create-release

# Default: build all binaries
all: deps build
Expand Down Expand Up @@ -301,6 +304,48 @@ dev-ca:
@echo "CA key generated: dev/ca_key"
@echo "CA public key: dev/ca_key.pub"

# =============================================================================
# Changelog and Release Management
# =============================================================================

# Generate changelog preview from last tag to HEAD
changelog-preview:
@echo "Generating changelog preview..."
@./scripts/generate-changelog

# Update CHANGELOG.md with unreleased changes (same as CI does)
changelog:
@echo "Updating CHANGELOG.md with unreleased changes..."
@LAST_TAG=$$(git describe --tags --abbrev=0 2>/dev/null || git rev-list --max-parents=0 HEAD); \
./scripts/generate-changelog "$$LAST_TAG" HEAD > /tmp/changelog-unreleased.md
@echo "Preview:"
@cat /tmp/changelog-unreleased.md
@echo ""
@read -p "Update CHANGELOG.md? [y/N] " -n 1 -r; echo; \
if [[ $$REPLY =~ ^[Yy]$$ ]]; then \
cp CHANGELOG.md CHANGELOG.md.bak; \
Comment on lines +323 to +326
Copy link

Copilot AI Jan 11, 2026

Choose a reason for hiding this comment

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

The changelog target writes to a predictable file in /tmp (/tmp/changelog-unreleased.md), which can be exploited on multi-user systems when run with elevated privileges: another user can pre-create this path as a symlink to an arbitrary file (e.g., /etc/passwd), causing that file to be truncated or overwritten when the redirect occurs. To avoid this, use a securely created temporary file (e.g., via mktemp in a private directory or within the repository) rather than a fixed name under a world-writable directory like /tmp.

Copilot uses AI. Check for mistakes.
{ \
sed -n '1,/## \[Unreleased\]/p' CHANGELOG.md | head -n -1; \
cat /tmp/changelog-unreleased.md; \
echo ""; \
sed -n '/## \[Unreleased\]/,$$ { /## \[Unreleased\]/d; /^$$/d; p }' CHANGELOG.md | sed '1,/^## \[/d' | sed '1s/^/\n/'; \
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

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

This complex sed pipeline for updating CHANGELOG.md is duplicated across multiple places (here, in the workflow, and in create-release script). The logic is fragile and hard to maintain. Consider extracting this into a dedicated script or function that can be reused consistently across all three locations.

Copilot uses AI. Check for mistakes.
} > CHANGELOG.md.tmp && mv CHANGELOG.md.tmp CHANGELOG.md; \
rm CHANGELOG.md.bak; \
echo "✓ CHANGELOG.md updated"; \
else \
echo "Cancelled"; \
fi

# Create a new release (interactive)
# Usage: make create-release VERSION=1.2.0
create-release:
@if [ -z "$(VERSION)" ]; then \
echo "Error: VERSION not specified"; \
echo "Usage: make create-release VERSION=1.2.0"; \
exit 1; \
fi
@./scripts/create-release $(VERSION)

# =============================================================================
# Clean
# =============================================================================
Expand Down
Loading