diff --git a/.github/workflows/auto-tag.yaml b/.github/workflows/auto-tag.yaml new file mode 100644 index 0000000..731bc09 --- /dev/null +++ b/.github/workflows/auto-tag.yaml @@ -0,0 +1,59 @@ +name: Auto Tag on Version Change + +on: + push: + branches: + - main + paths: + - 'package.json' + +jobs: + auto-tag: + runs-on: ubuntu-latest + + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all history for tag checking + + - name: Get version from package.json + id: package-version + run: | + VERSION=$(node -p "require('./package.json').version") + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "tag=v$VERSION" >> $GITHUB_OUTPUT + + - name: Check if tag exists + id: check-tag + run: | + if git rev-parse "v${{ steps.package-version.outputs.version }}" >/dev/null 2>&1; then + echo "exists=true" >> $GITHUB_OUTPUT + else + echo "exists=false" >> $GITHUB_OUTPUT + fi + + - name: Create and push tag + if: steps.check-tag.outputs.exists == 'false' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + TAG="${{ steps.package-version.outputs.tag }}" + # Try to create the tag; if it already exists locally, continue. + git tag -a "$TAG" -m "Release $TAG" || echo "Tag $TAG already exists locally, continuing." + # Try to push the tag. If the push fails, check if the tag now exists on the remote. + if ! git push origin "$TAG"; then + if git ls-remote --tags origin "$TAG" | grep -q "$TAG"; then + echo "Tag $TAG already exists on remote (likely created concurrently), skipping." + else + echo "Failed to push tag $TAG to remote." + exit 1 + fi + fi + + - name: Tag already exists + if: steps.check-tag.outputs.exists == 'true' + run: | + echo "Tag ${{ steps.package-version.outputs.tag }} already exists, skipping" diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml deleted file mode 100644 index db3968a..0000000 --- a/.github/workflows/create-release.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: Create Release - -on: - workflow_dispatch: - inputs: - release_type: - description: 'Release type (major, minor, patch)' - required: true - default: 'patch' - type: choice - options: - - patch - - minor - - major - -jobs: - version-and-release: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'npm' - - - name: Install dependencies - run: npm install - - - name: Bump version - id: bump_version - run: | - new_version=$(npm version ${{ github.event.inputs.release_type }} --git-tag-version false) - echo "new_version=$new_version" >> $GITHUB_ENV - - - name: Commit and Tag - env: - NEW_VERSION: ${{ env.new_version }} - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git commit -am "chore: Bump version to $NEW_VERSION" - git tag $NEW_VERSION - git push origin main --tags - - - name: Create GitHub Release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - uses: softprops/action-gh-release@v1 - with: - tag_name: ${{ env.new_version }} - body: 'Automated release ${{ env.new_version }}.' diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index fb2d846..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Publish to npm - -on: - release: - types: - - published - -jobs: - publish: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'npm' - - - name: Authenticate to npm - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - run: echo "Logged in to npm" - - - name: Install dependencies - run: npm install - - - name: Publish to npm - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - run: npm publish diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..ad39bb2 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,42 @@ +name: Publish to npm + +on: + push: + tags: + - 'v*' + +jobs: + publish: + runs-on: ubuntu-latest + + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + + - name: Verify tagged commit is in main branch history + run: | + git fetch origin main + if ! git merge-base --is-ancestor $GITHUB_SHA origin/main; then + echo "Tag is not based on main. Aborting release." + exit 1 + fi + + - uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: https://registry.npmjs.org + + - run: npm ci + - run: npm test + + - name: Publish to npm + run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..78163c0 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,146 @@ +# Release Process + +This document describes how releases of **aep-openapi-linter** are created and +published to both **npm** and **GitHub Releases**. + +The goal of this process is to be **lightweight, reliable, and easy to reason +about**, with minimal tooling and clear sources of truth. + +--- + +## Overview + +- Use `npm run release:{patch,minor,major}` to create a release branch. +- Push that branch and open a PR to main. +- Merging that branch triggers workflows that create a tag and publish the + release. + - The auto-tag workflow creates the new tag. + - The release workflow sees the new tag and publishes the npm and GitHub + releases. + +No release bots, changelog generators, or additional CLIs are required. + +--- + +## Versioning + +This project follows **Semantic Versioning (SemVer)**: + +- **Patch** (`x.y.z`) — bug fixes, performance improvements, internal refactors +- **Minor** (`x.y.0`) — new rules or non-breaking enhancements +- **Major** (`x.0.0`) — breaking changes (rule behavior changes, removals, + stricter defaults) + +Versions can be updated manually in package.json or using npm’s built-in +tooling. + +--- + +## Creating a Release + +**Note**: The main branch is protected, so releases must be created via Pull +Request. + +Run the release preparation script: + +```bash +npm run release:patch # or release:minor / release:major +``` + +This script will: + +- Ensure you're on main and up to date +- Verify the working tree is clean +- Run tests +- Bump the version in package.json and package-lock.json +- Create a release branch with the new version +- Commit and push the changes + +Then create a Pull Request to merge the release branch into main. + +Once the PR is merged, **the tag is created automatically**. + +The `auto-tag` workflow monitors package.json changes on main and automatically +creates the corresponding git tag if it doesn't exist. + +When the tag is created on main, this triggers the `release` workflow, which +will create the release in npm and in GitHub. + +--- + +## Automation (GitHub Actions) + +### Automatic Tagging + +The `auto-tag` workflow monitors package.json changes on the main branch. When +a version change is detected, it automatically: + +1. Reads the version from package.json +2. Checks if a tag for that version already exists +3. Creates and pushes the tag if it doesn't exist + +This eliminates the manual tagging step after PR merge. + +### Publishing and Releases + +The `release` workflow listens for pushed tags matching `v*` on the main +branch. + +On tag push, it will: + +1. Check out the repository +2. Install dependencies +3. Run tests +4. Publish the package to npm +5. Create a GitHub Release for the tag + +The GitHub Release is only created if npm publishing succeeds. + +--- + +## GitHub Release Behavior + +- Release name matches the tag (e.g. `v1.4.0`) +- Release notes are auto-generated from commits by GitHub +- Releases are created using the built-in `GITHUB_TOKEN` + +This ensures every published npm version has a visible, traceable release in +GitHub. + +--- + +## npm Configuration + +Publishing requires: + +- An npm automation token stored as a GitHub secret named `NPM_TOKEN` +- `publishConfig.access` set appropriately in `package.json` + +Example: + +```json +{ + "publishConfig": { + "access": "public" + } +} +``` + +--- + +## Optional: Changelog + +A manual `CHANGELOG.md` may be maintained if desired. + +If present, it can be used as the GitHub Release body in the future, but this +is intentionally not automated to keep the release process simple and +transparent. + +--- + +## Recovery and Rollbacks + +- If npm publishing fails, no GitHub Release is created +- The tag can be deleted and recreated if necessary +- Fixes should be released as a follow-up patch version rather than overwriting + published artifacts diff --git a/package-lock.json b/package-lock.json index b7c5276..a82b4fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "aep-openapi-linter", - "version": "0.1.0", + "name": "@aep_dev/aep-openapi-linter", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "aep-openapi-linter", - "version": "0.1.0", + "name": "@aep_dev/aep-openapi-linter", + "version": "0.5.0", "license": "MIT", "dependencies": { "@stoplight/spectral-functions": "^1.10.1" @@ -81,6 +81,7 @@ "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.0", @@ -1754,6 +1755,7 @@ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1776,6 +1778,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -2223,6 +2226,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001669", "electron-to-chromium": "^1.5.41", @@ -2880,6 +2884,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3019,6 +3024,7 @@ "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", @@ -4939,6 +4945,7 @@ "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", "license": "MIT", + "peer": true, "engines": { "node": ">= 10.16.0" } diff --git a/package.json b/package.json index b06a51f..6769f38 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,21 @@ "version": "0.5.0", "description": "Linter for OpenAPI definitions to check compliance to AEPs", "main": "spectral.yaml", + "files": [ + "aep", + "functions", + "spectral.yaml" + ], + "publishConfig": { + "access": "public" + }, "scripts": { "lint": "prettier --check . && eslint --cache --quiet --ext '.js' functions test", "lint-fix": "prettier --write . && eslint --cache --quiet --ext '.js' --fix functions test", - "test": "jest --coverage" + "test": "jest --coverage", + "release:patch": "./scripts/prepare-release.sh patch", + "release:minor": "./scripts/prepare-release.sh minor", + "release:major": "./scripts/prepare-release.sh major" }, "author": "Mike Kistler", "license": "MIT", diff --git a/scripts/prepare-release.sh b/scripts/prepare-release.sh new file mode 100755 index 0000000..d72a9b4 --- /dev/null +++ b/scripts/prepare-release.sh @@ -0,0 +1,94 @@ +#!/bin/bash +set -e + +# Usage: ./scripts/prepare-release.sh [patch|minor|major] + +if [ -z "$1" ]; then + echo "Usage: $0 [patch|minor|major]" + exit 1 +fi + +VERSION_TYPE=$1 + +if [[ ! "$VERSION_TYPE" =~ ^(patch|minor|major)$ ]]; then + echo "Error: Version type must be 'patch', 'minor', or 'major'" + exit 1 +fi + +# Ensure we're on main and up to date +echo "Checking current branch..." +current_branch=$(git rev-parse --abbrev-ref HEAD || echo "UNKNOWN") + +if [ "$current_branch" != "main" ]; then + echo "Error: This script must be run from the 'main' branch." + echo "Current branch: $current_branch" + echo "Please checkout 'main' and re-run this script." + exit 1 +fi + +echo "Verifying 'origin' remote..." +ORIGIN_URL=$(git remote get-url origin 2>/dev/null || echo "") +EXPECTED_REPO="aep-dev/aep-openapi-linter" + +if [[ ! "$ORIGIN_URL" =~ $EXPECTED_REPO ]]; then + echo "Error: 'origin' remote does not point to the main repository." + echo "Expected: $EXPECTED_REPO" + echo "Found: $ORIGIN_URL" + echo "Please ensure 'origin' points to the upstream repository, not a fork." + exit 1 +fi + +echo "Fetching latest from origin..." +git fetch origin main + +echo "Verifying branch is up to date..." +LOCAL=$(git rev-parse main) +REMOTE=$(git rev-parse origin/main) + +if [ "$LOCAL" != "$REMOTE" ]; then + echo "Error: Local 'main' branch is not in sync with 'origin/main'." + echo "Please pull the latest changes (git pull) and re-run this script." + exit 1 +fi + +# Check if working tree is clean +if [ -n "$(git status --porcelain)" ]; then + echo "Error: Working tree is not clean. Please commit or stash changes." + exit 1 +fi + +# Run tests +echo "Running tests..." +npm test + +# Run linter +echo "Running linter..." +npm run lint +# Bump version +echo "Bumping version ($VERSION_TYPE)..." +npm version "$VERSION_TYPE" --no-git-tag-version + +# Get the new version +NEW_VERSION=$(node -p "require('./package.json').version") +BRANCH_NAME="release/v$NEW_VERSION" + +echo "Creating release branch: $BRANCH_NAME" +git checkout -b "$BRANCH_NAME" + +# Add and commit +echo "Committing version bump..." +git add package.json package-lock.json +git commit -m "chore: bump version to v$NEW_VERSION" + +# Push branch +echo "Pushing branch to origin..." +git push origin "$BRANCH_NAME" + +echo "" +echo "✓ Release branch created and pushed!" +echo " Branch: $BRANCH_NAME" +echo " Version: v$NEW_VERSION" +echo "" +echo "Next steps:" +echo " 1. Create a Pull Request to merge $BRANCH_NAME into main" +echo " 2. Once merged, the tag will be created automatically"