From 5f2ac6e3ddda7fe2597b6ba74be8150522778548 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Wed, 28 Jan 2026 09:27:17 -0600 Subject: [PATCH 1/4] New publish workflow with complete documentation --- .github/workflows/auto-tag.yaml | 49 +++++++++ .github/workflows/create-release.yml | 55 ---------- .github/workflows/publish.yml | 33 ------ .github/workflows/release.yaml | 42 ++++++++ RELEASING.md | 146 +++++++++++++++++++++++++++ package-lock.json | 15 ++- package.json | 13 ++- scripts/prepare-release.sh | 60 +++++++++++ 8 files changed, 320 insertions(+), 93 deletions(-) create mode 100644 .github/workflows/auto-tag.yaml delete mode 100644 .github/workflows/create-release.yml delete mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/release.yaml create mode 100644 RELEASING.md create mode 100755 scripts/prepare-release.sh diff --git a/.github/workflows/auto-tag.yaml b/.github/workflows/auto-tag.yaml new file mode 100644 index 0000000..6597ad3 --- /dev/null +++ b/.github/workflows/auto-tag.yaml @@ -0,0 +1,49 @@ +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" + git tag -a "${{ steps.package-version.outputs.tag }}" -m "Release ${{ steps.package-version.outputs.tag }}" + git push origin "${{ steps.package-version.outputs.tag }}" + + - 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..8fbd39d --- /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: Ensure tag is on main + 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..22f0ef5 --- /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 +- 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..1c16b88 --- /dev/null +++ b/scripts/prepare-release.sh @@ -0,0 +1,60 @@ +#!/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 out main branch..." +git checkout main +git pull + +# 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 + +# 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" From 6b844ee1f6bb3c338102693de5f7a231e30eb050 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Wed, 28 Jan 2026 10:15:43 -0600 Subject: [PATCH 2/4] Apply copilot suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/auto-tag.yaml | 14 ++++++++++++-- .github/workflows/release.yaml | 2 +- RELEASING.md | 2 +- scripts/prepare-release.sh | 28 ++++++++++++++++++++++++---- 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/.github/workflows/auto-tag.yaml b/.github/workflows/auto-tag.yaml index 6597ad3..d4e2425 100644 --- a/.github/workflows/auto-tag.yaml +++ b/.github/workflows/auto-tag.yaml @@ -40,8 +40,18 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git tag -a "${{ steps.package-version.outputs.tag }}" -m "Release ${{ steps.package-version.outputs.tag }}" - git push origin "${{ steps.package-version.outputs.tag }}" + 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' diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 8fbd39d..ad39bb2 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Ensure tag is on main + - 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 diff --git a/RELEASING.md b/RELEASING.md index 22f0ef5..78163c0 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -52,7 +52,7 @@ 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 +- Bump the version in package.json and package-lock.json - Create a release branch with the new version - Commit and push the changes diff --git a/scripts/prepare-release.sh b/scripts/prepare-release.sh index 1c16b88..fabcaee 100755 --- a/scripts/prepare-release.sh +++ b/scripts/prepare-release.sh @@ -16,10 +16,27 @@ if [[ ! "$VERSION_TYPE" =~ ^(patch|minor|major)$ ]]; then fi # Ensure we're on main and up to date -echo "Checking out main branch..." -git checkout main -git pull +echo "Ensuring we are on 'main' branch..." +current_branch=$(git rev-parse --abbrev-ref HEAD || echo "UNKNOWN") +if [ "$current_branch" = "HEAD" ]; then + echo "Error: You are in a detached HEAD state. Please check out 'main' manually and re-run this script." + exit 1 +fi + +if [ "$current_branch" != "main" ]; then + echo "Checking out 'main' branch from '$current_branch'..." + if ! git checkout main; then + echo "Error: Failed to check out 'main'. Ensure you have no uncommitted changes blocking checkout and that the 'main' branch exists." + exit 1 + fi +fi + +echo "Updating 'main' from origin..." +if ! git pull --rebase; then + echo "Error: 'git pull' failed. Please resolve any issues (e.g., merge conflicts or network problems) 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." @@ -30,9 +47,12 @@ fi 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 +npm version "$VERSION_TYPE" --no-git-tag-version # Get the new version NEW_VERSION=$(node -p "require('./package.json').version") From 44f905b9d60906289bc064770a37e433743e0cc4 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Wed, 28 Jan 2026 11:31:38 -0600 Subject: [PATCH 3/4] Add a bit more bullet-proofing to prepare-release script --- scripts/prepare-release.sh | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/scripts/prepare-release.sh b/scripts/prepare-release.sh index fabcaee..d72a9b4 100755 --- a/scripts/prepare-release.sh +++ b/scripts/prepare-release.sh @@ -16,27 +16,41 @@ if [[ ! "$VERSION_TYPE" =~ ^(patch|minor|major)$ ]]; then fi # Ensure we're on main and up to date -echo "Ensuring we are on 'main' branch..." +echo "Checking current branch..." current_branch=$(git rev-parse --abbrev-ref HEAD || echo "UNKNOWN") -if [ "$current_branch" = "HEAD" ]; then - echo "Error: You are in a detached HEAD state. Please check out 'main' manually and re-run this script." +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 -if [ "$current_branch" != "main" ]; then - echo "Checking out 'main' branch from '$current_branch'..." - if ! git checkout main; then - echo "Error: Failed to check out 'main'. Ensure you have no uncommitted changes blocking checkout and that the 'main' branch exists." - 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 "Updating 'main' from origin..." -if ! git pull --rebase; then - echo "Error: 'git pull' failed. Please resolve any issues (e.g., merge conflicts or network problems) and re-run this script." +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." From 10cb87bd7aafc8b5398bb6d1090f55a5a7f968a8 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Wed, 28 Jan 2026 11:32:41 -0600 Subject: [PATCH 4/4] Fix lint errors --- .github/workflows/auto-tag.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/auto-tag.yaml b/.github/workflows/auto-tag.yaml index d4e2425..731bc09 100644 --- a/.github/workflows/auto-tag.yaml +++ b/.github/workflows/auto-tag.yaml @@ -10,14 +10,14 @@ on: 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 + fetch-depth: 0 # Fetch all history for tag checking - name: Get version from package.json id: package-version