diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 0000000..c26900c --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Self-contained pre-push hook for automatic semver patch version bumping +# Triggers on push to main/master, creates version bump commit + tag +# +# Setup: Copy to .githooks/pre-push and add to package.json: +# "scripts": { "prepare": "git config core.hooksPath .githooks" } +# +# Skip version bump: Include [skip-version] in your commit message + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +log_info() { echo -e "${GREEN}[version-bump]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[version-bump]${NC} $1"; } +log_error() { echo -e "${RED}[version-bump]${NC} $1" >&2; } + +# Get the branch being pushed +BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "") + +# Only bump on main/master +if [[ "$BRANCH" != "main" && "$BRANCH" != "master" ]]; then + exit 0 +fi + +# Get last commit message +LAST_MSG=$(git log -1 --format=%s 2>/dev/null || echo "") + +# Skip if this is already a version bump commit (prevents infinite loop) +if [[ "$LAST_MSG" == chore:\ bump\ version* ]]; then + log_info "Skipping: already a version bump commit" + exit 0 +fi + +# Skip if commit message contains [skip-version] +if [[ "$LAST_MSG" == *"[skip-version]"* ]]; then + log_info "Skipping: [skip-version] found in commit message" + exit 0 +fi + +# Find git root and package.json +GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo "") +if [[ -z "$GIT_ROOT" ]]; then + log_error "Not in a git repository" + exit 1 +fi + +PACKAGE_JSON="$GIT_ROOT/package.json" +if [[ ! -f "$PACKAGE_JSON" ]]; then + log_warn "No package.json found at git root, skipping version bump" + exit 0 +fi + +# Extract current version using portable tools +CURRENT_VERSION=$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$PACKAGE_JSON" | head -1 | sed 's/.*"\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)".*/\1/') + +if [[ -z "$CURRENT_VERSION" ]]; then + log_error "Could not parse version from package.json" + exit 1 +fi + +# Parse semver components +IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION" + +# Bump patch version +NEW_PATCH=$((PATCH + 1)) +NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}" + +log_info "Pushing to $BRANCH - bumping version: $CURRENT_VERSION -> $NEW_VERSION" + +# Update package.json (preserve formatting, just replace version) +if [[ "$(uname)" == "Darwin" ]]; then + # macOS sed requires empty string for -i + sed -i '' "s/\"version\"[[:space:]]*:[[:space:]]*\"$CURRENT_VERSION\"/\"version\": \"$NEW_VERSION\"/" "$PACKAGE_JSON" +else + # GNU sed + sed -i "s/\"version\"[[:space:]]*:[[:space:]]*\"$CURRENT_VERSION\"/\"version\": \"$NEW_VERSION\"/" "$PACKAGE_JSON" +fi + +log_info "Updated $PACKAGE_JSON" + +# Git commit +git add "$PACKAGE_JSON" +COMMIT_MSG="chore: bump version to $NEW_VERSION" +git commit -m "$COMMIT_MSG" +log_info "Created commit: $COMMIT_MSG" + +# Git tag +TAG_NAME="v$NEW_VERSION" +if git tag -l "$TAG_NAME" | grep -q "$TAG_NAME"; then + log_warn "Tag $TAG_NAME already exists, skipping" +else + git tag -a "$TAG_NAME" -m "Release $NEW_VERSION" + log_info "Created tag: $TAG_NAME" +fi + +log_info "Version bump complete!" +exit 0 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 63f1874..8bc0048 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,14 +1,55 @@ stages: - publish +variables: + GITLAB_NPM_REGISTRY: "https://gitlab.com/api/v4/packages/npm" + GITLAB_PROJECT_REGISTRY: "https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/npm" + publish: stage: publish image: node:20-alpine before_script: + - apk add --no-cache jq curl - corepack enable pnpm script: - - echo "@transquinnftw:registry=https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/npm/" > .npmrc - - echo "//gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${CI_JOB_TOKEN}" >> .npmrc - - pnpm publish --no-git-checks + # Configure for publishing to project-specific registry + - echo "@transquinnftw:registry=${GITLAB_PROJECT_REGISTRY}/" > .npmrc + - echo "//${GITLAB_PROJECT_REGISTRY#https://}/:_authToken=${CI_JOB_TOKEN}" >> .npmrc + # Get current version to check if already published + - export PKG_NAME=$(jq -r '.name' package.json) + - export PKG_VERSION=$(jq -r '.version' package.json) + - | + echo "Checking if ${PKG_NAME}@${PKG_VERSION} exists..." + HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ + --header "PRIVATE-TOKEN: ${CI_JOB_TOKEN}" \ + "${GITLAB_NPM_REGISTRY}/${PKG_NAME}/-/${PKG_NAME##*/}-${PKG_VERSION}.tgz" || echo "000") + if [ "$HTTP_STATUS" = "200" ]; then + echo "Version ${PKG_VERSION} already published, skipping" + exit 0 + fi + # Transform workspace:* to caret ranges for publishing + - | + node -e " + const fs = require('fs'); + const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); + const transform = (deps) => { + if (!deps) return deps; + for (const [name, version] of Object.entries(deps)) { + if (version === 'workspace:*' || version.startsWith('workspace:')) { + deps[name] = '*'; + } + } + return deps; + }; + pkg.dependencies = transform(pkg.dependencies); + pkg.devDependencies = transform(pkg.devDependencies); + pkg.peerDependencies = transform(pkg.peerDependencies); + // Remove publishConfig if it has unresolved variables + if (pkg.publishConfig?.['@transquinnftw:registry']?.includes('\${')) { + delete pkg.publishConfig; + } + fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2)); + " + - pnpm publish --no-git-checks --access public rules: - if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "master" diff --git a/package.json b/package.json index af7bd7d..089fd01 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,7 @@ { + "scripts": { + "prepare": "git config core.hooksPath .githooks" + }, "name": "@transquinnftw/typescript-config-react", "version": "1.0.1", "description": "TypeScript configuration for React applications with JSX support", @@ -19,6 +22,6 @@ "author": "", "license": "MIT", "peerDependencies": { - "@transquinnftw/typescript-config-base": "*" + "@transquinnftw/typescript-config-base": "workspace:*" } }