From 15352c07d2fa6100c7537297d2f46986d9eb0959 Mon Sep 17 00:00:00 2001 From: Lantum-Brendan Date: Tue, 16 Dec 2025 13:18:04 +0100 Subject: [PATCH 1/3] feat: Add javascript workflow --- README.md | 115 ++++++++++++ javascript/ci/action.yml | 139 ++++++++++++++ javascript/release/action.yml | 329 ++++++++++++++++++++++++++++++++++ 3 files changed, 583 insertions(+) create mode 100644 javascript/ci/action.yml create mode 100644 javascript/release/action.yml diff --git a/README.md b/README.md index 99a9cc0..9c4d066 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@ workflows/ ├── php/ │ ├── pre-release/ # Pre-release validation │ └── release/ # Create PHP package release +├── javascript/ +│ ├── ci/ # Lint, test, typecheck, build for Node.js/npm libs +│ └── release/ # Conventional-commit-driven npm release ├── package/ # [DEPRECATED] Use php/ instead │ ├── pre-release/ │ └── release/ @@ -197,6 +200,118 @@ jobs: --- +## JavaScript / Node.js Workflows + +### CI (`javascript/ci`) + +Runs lint, tests, optional typecheck, and optional build for Node.js/npm libraries. + +**Usage:** + +```yaml +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +jobs: + ci: + runs-on: ubuntu-latest + steps: + - name: JavaScript CI + uses: whilesmart/workflows/javascript/ci@main + with: + token: ${{ secrets.GITHUB_TOKEN }} + # Optional overrides: + # node_version: '20.x' + # working_directory: '.' + # lint_script: 'lint' + # run_tests: 'true' +``` + +**Requirements:** + +1. `package.json` in the working directory. +2. NPM scripts: + - `lint` (or the script name passed via `lint_script`). + - `typecheck` (mandatory; can be a no-op if you are not using TypeScript). + - `build` (mandatory; used to validate that the library can be built). + - `test` or `test:ci` (required when `run_tests` is set to `'true'`, which is the default). + +--- + +### Release (`javascript/release`) + +Creates a new npm release for Node.js libraries based on conventional commits. + +**Features:** +- Determines `patch`/`minor`/`major` bump from commit history. +- Updates `package.json` version and prepends an entry to `CHANGELOG.md`. +- Commits changes, creates a `vX.Y.Z` tag, publishes to npm, and creates a GitHub Release. +- Supports `dry_run` mode and custom npm `dist-tag` (e.g., `latest`, `next`, `beta`). + +**Usage (release from main):** + +```yaml +name: Release + +on: + push: + branches: [main] + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: JavaScript Release + uses: whilesmart/workflows/javascript/release@main + with: + token: ${{ secrets.GITHUB_TOKEN }} + npm_token: ${{ secrets.NPM_TOKEN }} + # Optional overrides: + # node_version: '20.x' + # working_directory: '.' + # release_branch: 'main' + # npm_registry: 'https://registry.npmjs.org/' + # dist_tag: 'latest' + # dry_run: 'false' +``` + +**Optional: pre-release / next channel** + +```yaml +on: + push: + branches: [next] + +jobs: + release-next: + runs-on: ubuntu-latest + steps: + - name: JavaScript Release (next) + uses: whilesmart/workflows/javascript/release@main + with: + token: ${{ secrets.GITHUB_TOKEN }} + npm_token: ${{ secrets.NPM_TOKEN }} + release_branch: 'next' + dist_tag: 'next' +``` + +**Requirements:** + +1. `package.json` with a valid `version` field. +2. Conventional commits used on the release branch (`feat`, `fix`, `BREAKING CHANGE`, etc.). +3. `CHANGELOG.md` (optional but recommended; it will be created or prepended to if present). +4. `NPM_TOKEN` secret configured with publish permissions for the target npm registry. + +--- + ## Adding New Languages To add workflows for a new language: diff --git a/javascript/ci/action.yml b/javascript/ci/action.yml new file mode 100644 index 0000000..f90d097 --- /dev/null +++ b/javascript/ci/action.yml @@ -0,0 +1,139 @@ +name: 'JavaScript CI' +description: 'Run lint, tests, optional typecheck and build for Node.js/npm projects' + +inputs: + token: + description: 'GitHub token for authentication' + required: true + node_version: + description: 'Node.js version to use' + required: false + default: 'lts/*' + working_directory: + description: 'Directory containing package.json (relative to repository root)' + required: false + default: '.' + lint_script: + description: 'NPM script name to run for linting' + required: false + default: 'lint' + run_tests: + description: 'Whether to run the test script (test or test:ci)' + required: false + default: 'true' + run_typecheck: + description: 'Whether to run the typecheck script (optional for non-TS projects)' + required: false + default: 'false' + run_build: + description: 'Whether to run the build script' + required: false + default: 'false' + +runs: + using: 'composite' + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ inputs.token }} + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node_version }} + cache: 'npm' + cache-dependency-path: ${{ inputs.working_directory }}/package-lock.json + + - name: Install dependencies + shell: bash + working-directory: ${{ inputs.working_directory }} + run: | + if [ ! -f "package.json" ]; then + echo "❌ package.json not found in $PWD" + exit 1 + fi + + if [ -f "package-lock.json" ]; then + echo "📦 Using npm ci" + npm ci + else + echo "📦 Using npm install" + npm install + fi + + - name: Run lint + shell: bash + working-directory: ${{ inputs.working_directory }} + run: | + if [ ! -f "package.json" ]; then + echo "❌ package.json not found in $PWD" + exit 1 + fi + + if jq -e ".scripts[\"${{ inputs.lint_script }}\"]" package.json >/dev/null; then + echo "🔎 Running npm run ${{ inputs.lint_script }}..." + npm run ${{ inputs.lint_script }} + else + echo "❌ No \"${{ inputs.lint_script }}\" script defined in package.json" + echo " Please add it or override the lint_script input." + exit 1 + fi + + - name: Run typecheck + if: ${{ inputs.run_typecheck == 'true' }} + shell: bash + working-directory: ${{ inputs.working_directory }} + run: | + if jq -e '.scripts["typecheck"]' package.json >/dev/null; then + echo "🔡 Running npm run typecheck..." + npm run typecheck + else + echo "⚠️ No \"typecheck\" script defined; skipping." + fi + + - name: Run tests + if: ${{ inputs.run_tests == 'true' }} + shell: bash + working-directory: ${{ inputs.working_directory }} + run: | + if [ ! -f "package.json" ]; then + echo "❌ package.json not found in $PWD" + exit 1 + fi + + TEST_SCRIPT="" + if jq -e '.scripts["test:ci"]' package.json >/dev/null; then + TEST_SCRIPT="test:ci" + elif jq -e '.scripts["test"]' package.json >/dev/null; then + TEST_SCRIPT="test" + else + echo "❌ No \"test\" or \"test:ci\" script defined in package.json" + echo " Please define one of them to use the JavaScript CI action." + exit 1 + fi + + echo "🧪 Running npm run $TEST_SCRIPT..." + npm run "$TEST_SCRIPT" + + - name: Run build + if: ${{ inputs.run_build == 'true' }} + shell: bash + working-directory: ${{ inputs.working_directory }} + run: | + if jq -e '.scripts["build"]' package.json >/dev/null; then + echo "🏗️ Running npm run build..." + npm run build + else + echo "⚠️ No \"build\" script defined; skipping." + fi + + - name: Summary + if: success() + shell: bash + run: | + echo "## ✅ JavaScript CI Summary" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "- Node.js version: \`${{ inputs.node_version }}\`" >> "$GITHUB_STEP_SUMMARY" + echo "- Working directory: \`${{ inputs.working_directory }}\`" >> "$GITHUB_STEP_SUMMARY" diff --git a/javascript/release/action.yml b/javascript/release/action.yml new file mode 100644 index 0000000..14b37b5 --- /dev/null +++ b/javascript/release/action.yml @@ -0,0 +1,329 @@ +name: 'JavaScript Release' +description: 'Conventional-commit-driven npm release for Node.js libraries' + +inputs: + token: + description: 'GitHub token for authentication (used for checkout, pushing tags, and creating releases)' + required: true + npm_token: + description: 'NPM auth token for publishing to the registry' + required: true + node_version: + description: 'Node.js version to use' + required: false + default: 'lts/*' + working_directory: + description: 'Directory containing package.json (relative to repository root)' + required: false + default: '.' + release_branch: + description: 'Branch from which releases are allowed (e.g., main, next)' + required: false + default: 'main' + npm_registry: + description: 'NPM registry URL' + required: false + default: 'https://registry.npmjs.org/' + dist_tag: + description: 'npm dist-tag to publish under (e.g., latest, next, beta)' + required: false + default: 'latest' + dry_run: + description: 'If true, compute next version but do not change files, tag, or publish' + required: false + default: 'false' + +outputs: + released: + description: 'Whether a new version was released (true/false)' + value: ${{ steps.determine_release.outputs.should_release }} + new_version: + description: 'The new version that was computed (if any)' + value: ${{ steps.determine_release.outputs.new_version }} + tag: + description: 'The git tag that was created (if any)' + value: ${{ steps.tag_and_push.outputs.tag_name }} + release_url: + description: 'URL of the created GitHub release (if any)' + value: ${{ steps.create_release.outputs.url }} + +runs: + using: 'composite' + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ inputs.token }} + + - name: Configure Git + shell: bash + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + + - name: Ensure running on release branch + shell: bash + run: | + CURRENT_REF="${GITHUB_REF#refs/heads/}" + if [ "$CURRENT_REF" != "${{ inputs.release_branch }}" ]; then + echo "❌ JavaScript Release can only run on branch '${{ inputs.release_branch }}' (current: '$CURRENT_REF')" + exit 1 + fi + + - name: Determine next version from conventional commits + id: determine_release + shell: bash + working-directory: ${{ inputs.working_directory }} + run: | + if [ ! -f "package.json" ]; then + echo "❌ package.json not found in $PWD" + exit 1 + fi + + CURRENT_VERSION=$(jq -r '.version // "0.0.0"' package.json) + if [ -z "$CURRENT_VERSION" ] || [ "$CURRENT_VERSION" = "null" ]; then + CURRENT_VERSION="0.0.0" + fi + + # Validate semantic version format + if ! [[ "$CURRENT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+)?$ ]]; then + echo "❌ Invalid semver format: $CURRENT_VERSION" + exit 1 + fi + + LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + FROM_REF="$LAST_TAG" + + echo "Current version in package.json: $CURRENT_VERSION" + if [ -n "$LAST_TAG" ]; then + echo "Last tag: $LAST_TAG" + RANGE="$LAST_TAG..HEAD" + else + echo "No existing tags found; using full history" + RANGE="" + fi + + if [ -n "$RANGE" ]; then + COMMITS=$(git log "$RANGE" --pretty=format:'%s%n%b') + else + COMMITS=$(git log --pretty=format:'%s%n%b') + fi + + if [ -z "$COMMITS" ]; then + echo "ℹ️ No commits found to consider for release. Skipping release." + echo "should_release=false" >> $GITHUB_OUTPUT + echo "new_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + echo "from_ref=$FROM_REF" >> $GITHUB_OUTPUT + exit 0 + fi + + BUMP_TYPE="none" + + if echo "$COMMITS" | grep -qE '(^|[^a-z])BREAKING CHANGE:|^feat(\(.+\))?!:'; then + BUMP_TYPE="major" + elif echo "$COMMITS" | grep -qE '^feat(\(.+\))?:'; then + BUMP_TYPE="minor" + elif echo "$COMMITS" | grep -qE '^fix(\(.+\))?:'; then + BUMP_TYPE="patch" + fi + + if [ "$BUMP_TYPE" = "none" ]; then + echo "ℹ️ No conventional-commit changes requiring a release were found." + echo "should_release=false" >> $GITHUB_OUTPUT + echo "new_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + echo "from_ref=$FROM_REF" >> $GITHUB_OUTPUT + exit 0 + fi + + IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION" + MAJOR=${MAJOR:-0} + MINOR=${MINOR:-0} + PATCH=${PATCH:-0} + + case "$BUMP_TYPE" in + major) + MAJOR=$((MAJOR + 1)) + MINOR=0 + PATCH=0 + ;; + minor) + MINOR=$((MINOR + 1)) + PATCH=0 + ;; + patch) + PATCH=$((PATCH + 1)) + ;; + esac + + NEW_VERSION="$MAJOR.$MINOR.$PATCH" + + echo "Recommended bump: $BUMP_TYPE" + echo "New version: $NEW_VERSION" + + echo "should_release=true" >> $GITHUB_OUTPUT + echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT + echo "from_ref=$FROM_REF" >> $GITHUB_OUTPUT + echo "bump_type=$BUMP_TYPE" >> $GITHUB_OUTPUT + + - name: Set up Node.js + if: ${{ steps.determine_release.outputs.should_release == 'true' && inputs.dry_run == 'false' }} + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node_version }} + registry-url: ${{ inputs.npm_registry }} + + - name: Update package.json version + if: ${{ steps.determine_release.outputs.should_release == 'true' && inputs.dry_run == 'false' }} + id: update_version + shell: bash + working-directory: ${{ inputs.working_directory }} + run: | + NEW_VERSION="${{ steps.determine_release.outputs.new_version }}" + echo "Updating package.json version to $NEW_VERSION" + tmpfile=$(mktemp) + jq --arg version "$NEW_VERSION" '.version = $version' package.json > "$tmpfile" + mv "$tmpfile" package.json + + - name: Update CHANGELOG.md + if: ${{ steps.determine_release.outputs.should_release == 'true' && inputs.dry_run == 'false' }} + id: update_changelog + shell: bash + working-directory: ${{ inputs.working_directory }} + run: | + NEW_VERSION="${{ steps.determine_release.outputs.new_version }}" + FROM_REF="${{ steps.determine_release.outputs.from_ref }}" + DATE="$(date -u '+%Y-%m-%d')" + + TEMP_FILE=$(mktemp) + + { + echo "## [$NEW_VERSION] - $DATE" + echo "" + if [ -n "$FROM_REF" ]; then + git log "$FROM_REF"..HEAD --pretty=format:'- %s' + else + git log --pretty=format:'- %s' + fi + echo "" + } > "$TEMP_FILE" + + if [ -f "CHANGELOG.md" ]; then + cat "CHANGELOG.md" >> "$TEMP_FILE" + fi + + mv "$TEMP_FILE" "CHANGELOG.md" + echo "✅ Updated CHANGELOG.md with $NEW_VERSION entry" + + - name: Commit version and changelog + if: ${{ steps.determine_release.outputs.should_release == 'true' && inputs.dry_run == 'false' }} + id: commit_changes + shell: bash + working-directory: ${{ inputs.working_directory }} + run: | + NEW_VERSION="${{ steps.determine_release.outputs.new_version }}" + + git add package.json + if [ -f "CHANGELOG.md" ]; then + git add CHANGELOG.md + fi + + if git diff --cached --quiet; then + echo "ℹ️ No changes to commit for version $NEW_VERSION" + else + git commit -m "chore(release): v$NEW_VERSION" + echo "✅ Committed release changes for v$NEW_VERSION" + fi + + - name: Create and push tag + if: ${{ steps.determine_release.outputs.should_release == 'true' && inputs.dry_run == 'false' }} + id: tag_and_push + shell: bash + working-directory: ${{ inputs.working_directory }} + run: | + NEW_VERSION="${{ steps.determine_release.outputs.new_version }}" + TAG="v$NEW_VERSION" + + if git rev-parse "$TAG" >/dev/null 2>&1; then + echo "❌ Tag $TAG already exists! Aborting release." + exit 1 + fi + + git tag -a "$TAG" -m "Release $TAG" + echo "✅ Created tag $TAG" + + git push origin HEAD + git push origin "$TAG" + echo "✅ Pushed branch and tag to origin" + + echo "tag_name=$TAG" >> $GITHUB_OUTPUT + + - name: Build and publish to npm + if: ${{ steps.determine_release.outputs.should_release == 'true' && inputs.dry_run == 'false' }} + shell: bash + env: + NODE_AUTH_TOKEN: ${{ inputs.npm_token }} + working-directory: ${{ inputs.working_directory }} + run: | + echo "📦 Installing dependencies before publish..." + if [ -f "package-lock.json" ]; then + npm ci + else + npm install + fi + + if jq -e '.scripts["build"]' package.json >/dev/null; then + echo "🏗️ Running npm run build before publish..." + npm run build + else + echo "ℹ️ No \"build\" script defined, publishing without build step." + fi + + echo "🚀 Publishing package to npm with dist-tag '${{ inputs.dist_tag }}'..." + npm publish --tag "${{ inputs.dist_tag }}" --registry "${{ inputs.npm_registry }}" + + - name: Create GitHub Release + if: ${{ steps.determine_release.outputs.should_release == 'true' && inputs.dry_run == 'false' }} + id: create_release + uses: softprops/action-gh-release@v1 + with: + tag_name: v${{ steps.determine_release.outputs.new_version }} + name: Release v${{ steps.determine_release.outputs.new_version }} + body: | + Automated release for v${{ steps.determine_release.outputs.new_version }}. + + See CHANGELOG.md for full details. + draft: false + prerelease: ${{ contains(steps.determine_release.outputs.new_version, '-') }} + token: ${{ inputs.token }} + + - name: Dry-run summary (no publish) + if: ${{ steps.determine_release.outputs.should_release == 'true' && inputs.dry_run == 'true' }} + shell: bash + run: | + NEW_VERSION="${{ steps.determine_release.outputs.new_version }}" + echo "## 🚧 JavaScript Release (dry-run)" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "- Recommended new version: \`$NEW_VERSION\`" >> "$GITHUB_STEP_SUMMARY" + echo "- No files, tags, or npm publish were performed because dry_run=true" >> "$GITHUB_STEP_SUMMARY" + + - name: No-release summary + if: ${{ steps.determine_release.outputs.should_release != 'true' }} + shell: bash + run: | + echo "## ℹ️ JavaScript Release" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "No conventional-commit changes requiring a release were detected. No new version was published." >> "$GITHUB_STEP_SUMMARY" + + - name: Release summary + if: ${{ steps.determine_release.outputs.should_release == 'true' && inputs.dry_run == 'false' }} + shell: bash + run: | + NEW_VERSION="${{ steps.determine_release.outputs.new_version }}" + TAG="v$NEW_VERSION" + echo "## 🎉 JavaScript Release Summary" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "- Released version: \`$NEW_VERSION\`" >> "$GITHUB_STEP_SUMMARY" + echo "- Git tag: \`$TAG\`" >> "$GITHUB_STEP_SUMMARY" + echo "- npm dist-tag: \`${{ inputs.dist_tag }}\`" >> "$GITHUB_STEP_SUMMARY" + echo "- Branch: \`${{ inputs.release_branch }}\`" >> "$GITHUB_STEP_SUMMARY" From 18fea3893b9733dfeab222958286f04f133f5172 Mon Sep 17 00:00:00 2001 From: Lantum-Brendan Date: Sat, 20 Dec 2025 14:36:57 +0100 Subject: [PATCH 2/3] fix: Enforce mandatory typecheck/build in CI&simplify release/version check --- README.md | 19 +-- javascript/ci/action.yml | 30 +--- javascript/release/action.yml | 270 +++++++++++----------------------- 3 files changed, 105 insertions(+), 214 deletions(-) diff --git a/README.md b/README.md index 9c4d066..032c915 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ workflows/ │ ├── pre-release/ # Pre-release validation │ └── release/ # Create PHP package release ├── javascript/ -│ ├── ci/ # Lint, test, typecheck, build for Node.js/npm libs -│ └── release/ # Conventional-commit-driven npm release +│ ├── ci/ # Lint, optional tests, typecheck, build for Node.js/npm libs +│ └── release/ # Publish npm package and create GitHub release ├── package/ # [DEPRECATED] Use php/ instead │ ├── pre-release/ │ └── release/ @@ -204,7 +204,7 @@ jobs: ### CI (`javascript/ci`) -Runs lint, tests, optional typecheck, and optional build for Node.js/npm libraries. +Runs lint, optional tests, and mandatory typecheck and build for Node.js/npm libraries. **Usage:** @@ -245,12 +245,13 @@ jobs: ### Release (`javascript/release`) -Creates a new npm release for Node.js libraries based on conventional commits. +Publishes a new npm release for Node.js libraries based on the `version` in `package.json`. **Features:** -- Determines `patch`/`minor`/`major` bump from commit history. -- Updates `package.json` version and prepends an entry to `CHANGELOG.md`. -- Commits changes, creates a `vX.Y.Z` tag, publishes to npm, and creates a GitHub Release. +- Reads the version from `package.json`. +- Optionally includes release notes from `CHANGELOG.md` (if a matching version entry exists). +- Creates and pushes a `vX.Y.Z` tag. +- Publishes to npm and creates a GitHub Release. - Supports `dry_run` mode and custom npm `dist-tag` (e.g., `latest`, `next`, `beta`). **Usage (release from main):** @@ -306,8 +307,8 @@ jobs: **Requirements:** 1. `package.json` with a valid `version` field. -2. Conventional commits used on the release branch (`feat`, `fix`, `BREAKING CHANGE`, etc.). -3. `CHANGELOG.md` (optional but recommended; it will be created or prepended to if present). +2. `build` script in `package.json` (mandatory; can be a no-op if your project does not need a build step). +3. `CHANGELOG.md` (optional but recommended; if present, include an entry like `## [X.Y.Z] - YYYY-MM-DD` for the version). 4. `NPM_TOKEN` secret configured with publish permissions for the target npm registry. --- diff --git a/javascript/ci/action.yml b/javascript/ci/action.yml index f90d097..e72ac9f 100644 --- a/javascript/ci/action.yml +++ b/javascript/ci/action.yml @@ -1,5 +1,5 @@ name: 'JavaScript CI' -description: 'Run lint, tests, optional typecheck and build for Node.js/npm projects' +description: 'Run lint, typecheck, optional tests, and build for Node.js/npm projects' inputs: token: @@ -21,14 +21,6 @@ inputs: description: 'Whether to run the test script (test or test:ci)' required: false default: 'true' - run_typecheck: - description: 'Whether to run the typecheck script (optional for non-TS projects)' - required: false - default: 'false' - run_build: - description: 'Whether to run the build script' - required: false - default: 'false' runs: using: 'composite' @@ -67,11 +59,6 @@ runs: shell: bash working-directory: ${{ inputs.working_directory }} run: | - if [ ! -f "package.json" ]; then - echo "❌ package.json not found in $PWD" - exit 1 - fi - if jq -e ".scripts[\"${{ inputs.lint_script }}\"]" package.json >/dev/null; then echo "🔎 Running npm run ${{ inputs.lint_script }}..." npm run ${{ inputs.lint_script }} @@ -82,7 +69,6 @@ runs: fi - name: Run typecheck - if: ${{ inputs.run_typecheck == 'true' }} shell: bash working-directory: ${{ inputs.working_directory }} run: | @@ -90,7 +76,9 @@ runs: echo "🔡 Running npm run typecheck..." npm run typecheck else - echo "⚠️ No \"typecheck\" script defined; skipping." + echo "❌ No \"typecheck\" script defined in package.json" + echo " Please add a typecheck script (it can be a no-op if you are not using TypeScript)." + exit 1 fi - name: Run tests @@ -98,11 +86,6 @@ runs: shell: bash working-directory: ${{ inputs.working_directory }} run: | - if [ ! -f "package.json" ]; then - echo "❌ package.json not found in $PWD" - exit 1 - fi - TEST_SCRIPT="" if jq -e '.scripts["test:ci"]' package.json >/dev/null; then TEST_SCRIPT="test:ci" @@ -118,7 +101,6 @@ runs: npm run "$TEST_SCRIPT" - name: Run build - if: ${{ inputs.run_build == 'true' }} shell: bash working-directory: ${{ inputs.working_directory }} run: | @@ -126,7 +108,9 @@ runs: echo "🏗️ Running npm run build..." npm run build else - echo "⚠️ No \"build\" script defined; skipping." + echo "❌ No \"build\" script defined in package.json" + echo " Please add a build script (it can be a no-op if your project does not need a build step)." + exit 1 fi - name: Summary diff --git a/javascript/release/action.yml b/javascript/release/action.yml index 14b37b5..7a279a8 100644 --- a/javascript/release/action.yml +++ b/javascript/release/action.yml @@ -1,5 +1,5 @@ name: 'JavaScript Release' -description: 'Conventional-commit-driven npm release for Node.js libraries' +description: 'Publish npm package and create a GitHub release based on package.json version' inputs: token: @@ -29,20 +29,17 @@ inputs: required: false default: 'latest' dry_run: - description: 'If true, compute next version but do not change files, tag, or publish' + description: 'If true, do not tag, publish, or create a GitHub Release' required: false default: 'false' outputs: - released: - description: 'Whether a new version was released (true/false)' - value: ${{ steps.determine_release.outputs.should_release }} - new_version: - description: 'The new version that was computed (if any)' - value: ${{ steps.determine_release.outputs.new_version }} + version: + description: 'The version being released (from package.json)' + value: ${{ steps.get_version.outputs.version }} tag: - description: 'The git tag that was created (if any)' - value: ${{ steps.tag_and_push.outputs.tag_name }} + description: 'The git tag for the release' + value: ${{ steps.get_version.outputs.tag }} release_url: description: 'URL of the created GitHub release (if any)' value: ${{ steps.create_release.outputs.url }} @@ -71,8 +68,8 @@ runs: exit 1 fi - - name: Determine next version from conventional commits - id: determine_release + - name: Extract version from package.json + id: get_version shell: bash working-directory: ${{ inputs.working_directory }} run: | @@ -81,185 +78,98 @@ runs: exit 1 fi - CURRENT_VERSION=$(jq -r '.version // "0.0.0"' package.json) - if [ -z "$CURRENT_VERSION" ] || [ "$CURRENT_VERSION" = "null" ]; then - CURRENT_VERSION="0.0.0" - fi + VERSION=$(jq -r '.version // empty' package.json) - # Validate semantic version format - if ! [[ "$CURRENT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+)?$ ]]; then - echo "❌ Invalid semver format: $CURRENT_VERSION" + if [ -z "$VERSION" ]; then + echo "❌ No version found in package.json" + echo " Please set package.json \"version\" before releasing." exit 1 fi - LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") - FROM_REF="$LAST_TAG" - - echo "Current version in package.json: $CURRENT_VERSION" - if [ -n "$LAST_TAG" ]; then - echo "Last tag: $LAST_TAG" - RANGE="$LAST_TAG..HEAD" - else - echo "No existing tags found; using full history" - RANGE="" - fi - - if [ -n "$RANGE" ]; then - COMMITS=$(git log "$RANGE" --pretty=format:'%s%n%b') - else - COMMITS=$(git log --pretty=format:'%s%n%b') - fi - - if [ -z "$COMMITS" ]; then - echo "ℹ️ No commits found to consider for release. Skipping release." - echo "should_release=false" >> $GITHUB_OUTPUT - echo "new_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT - echo "from_ref=$FROM_REF" >> $GITHUB_OUTPUT - exit 0 - fi - - BUMP_TYPE="none" - - if echo "$COMMITS" | grep -qE '(^|[^a-z])BREAKING CHANGE:|^feat(\(.+\))?!:'; then - BUMP_TYPE="major" - elif echo "$COMMITS" | grep -qE '^feat(\(.+\))?:'; then - BUMP_TYPE="minor" - elif echo "$COMMITS" | grep -qE '^fix(\(.+\))?:'; then - BUMP_TYPE="patch" - fi - - if [ "$BUMP_TYPE" = "none" ]; then - echo "ℹ️ No conventional-commit changes requiring a release were found." - echo "should_release=false" >> $GITHUB_OUTPUT - echo "new_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT - echo "from_ref=$FROM_REF" >> $GITHUB_OUTPUT - exit 0 + if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then + echo "❌ Invalid semver format: $VERSION" + exit 1 fi - IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION" - MAJOR=${MAJOR:-0} - MINOR=${MINOR:-0} - PATCH=${PATCH:-0} - - case "$BUMP_TYPE" in - major) - MAJOR=$((MAJOR + 1)) - MINOR=0 - PATCH=0 - ;; - minor) - MINOR=$((MINOR + 1)) - PATCH=0 - ;; - patch) - PATCH=$((PATCH + 1)) - ;; - esac - - NEW_VERSION="$MAJOR.$MINOR.$PATCH" - - echo "Recommended bump: $BUMP_TYPE" - echo "New version: $NEW_VERSION" - - echo "should_release=true" >> $GITHUB_OUTPUT - echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT - echo "from_ref=$FROM_REF" >> $GITHUB_OUTPUT - echo "bump_type=$BUMP_TYPE" >> $GITHUB_OUTPUT - - - name: Set up Node.js - if: ${{ steps.determine_release.outputs.should_release == 'true' && inputs.dry_run == 'false' }} - uses: actions/setup-node@v4 - with: - node-version: ${{ inputs.node_version }} - registry-url: ${{ inputs.npm_registry }} + TAG="v$VERSION" + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "tag=$TAG" >> $GITHUB_OUTPUT + echo "✅ Found version: $VERSION" - - name: Update package.json version - if: ${{ steps.determine_release.outputs.should_release == 'true' && inputs.dry_run == 'false' }} - id: update_version + - name: Extract changelog (optional) + id: get_changelog shell: bash working-directory: ${{ inputs.working_directory }} run: | - NEW_VERSION="${{ steps.determine_release.outputs.new_version }}" - echo "Updating package.json version to $NEW_VERSION" - tmpfile=$(mktemp) - jq --arg version "$NEW_VERSION" '.version = $version' package.json > "$tmpfile" - mv "$tmpfile" package.json + VERSION="${{ steps.get_version.outputs.version }}" - - name: Update CHANGELOG.md - if: ${{ steps.determine_release.outputs.should_release == 'true' && inputs.dry_run == 'false' }} - id: update_changelog - shell: bash - working-directory: ${{ inputs.working_directory }} - run: | - NEW_VERSION="${{ steps.determine_release.outputs.new_version }}" - FROM_REF="${{ steps.determine_release.outputs.from_ref }}" - DATE="$(date -u '+%Y-%m-%d')" + if [ ! -f "CHANGELOG.md" ]; then + echo "ℹ️ No CHANGELOG.md found, skipping release notes extraction" + echo "release_notes=Release $VERSION" >> $GITHUB_OUTPUT + exit 0 + fi TEMP_FILE=$(mktemp) - { - echo "## [$NEW_VERSION] - $DATE" - echo "" - if [ -n "$FROM_REF" ]; then - git log "$FROM_REF"..HEAD --pretty=format:'- %s' - else - git log --pretty=format:'- %s' - fi - echo "" - } > "$TEMP_FILE" - - if [ -f "CHANGELOG.md" ]; then - cat "CHANGELOG.md" >> "$TEMP_FILE" + awk -v version="$VERSION" ' + BEGIN { found=0; printing=0 } + /^## \[/ { + if (found && printing) exit + if ($0 ~ "\\[" version "\\]") { + found=1 + printing=1 + next + } + } + found && printing && /^## \[/ { exit } + found && printing { print } + ' CHANGELOG.md > "$TEMP_FILE" + + if [ ! -s "$TEMP_FILE" ]; then + echo "ℹ️ No changelog entry for version $VERSION" + echo "release_notes=Release $VERSION" >> $GITHUB_OUTPUT + else + { + echo 'release_notes<> $GITHUB_OUTPUT + echo "✅ Found changelog entry for version $VERSION" fi - mv "$TEMP_FILE" "CHANGELOG.md" - echo "✅ Updated CHANGELOG.md with $NEW_VERSION entry" + rm "$TEMP_FILE" - - name: Commit version and changelog - if: ${{ steps.determine_release.outputs.should_release == 'true' && inputs.dry_run == 'false' }} - id: commit_changes + - name: Check if tag already exists shell: bash - working-directory: ${{ inputs.working_directory }} run: | - NEW_VERSION="${{ steps.determine_release.outputs.new_version }}" - - git add package.json - if [ -f "CHANGELOG.md" ]; then - git add CHANGELOG.md + TAG="${{ steps.get_version.outputs.tag }}" + if git rev-parse "$TAG" >/dev/null 2>&1; then + echo "❌ Tag $TAG already exists!" + exit 1 fi + echo "✅ Tag $TAG does not exist, proceeding..." - if git diff --cached --quiet; then - echo "ℹ️ No changes to commit for version $NEW_VERSION" - else - git commit -m "chore(release): v$NEW_VERSION" - echo "✅ Committed release changes for v$NEW_VERSION" - fi + - name: Set up Node.js + if: ${{ inputs.dry_run == 'false' }} + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node_version }} + registry-url: ${{ inputs.npm_registry }} - name: Create and push tag - if: ${{ steps.determine_release.outputs.should_release == 'true' && inputs.dry_run == 'false' }} - id: tag_and_push + if: ${{ inputs.dry_run == 'false' }} shell: bash - working-directory: ${{ inputs.working_directory }} run: | - NEW_VERSION="${{ steps.determine_release.outputs.new_version }}" - TAG="v$NEW_VERSION" - - if git rev-parse "$TAG" >/dev/null 2>&1; then - echo "❌ Tag $TAG already exists! Aborting release." - exit 1 - fi + TAG="${{ steps.get_version.outputs.tag }}" git tag -a "$TAG" -m "Release $TAG" echo "✅ Created tag $TAG" - git push origin HEAD git push origin "$TAG" - echo "✅ Pushed branch and tag to origin" - - echo "tag_name=$TAG" >> $GITHUB_OUTPUT + echo "✅ Pushed tag to origin" - name: Build and publish to npm - if: ${{ steps.determine_release.outputs.should_release == 'true' && inputs.dry_run == 'false' }} + if: ${{ inputs.dry_run == 'false' }} shell: bash env: NODE_AUTH_TOKEN: ${{ inputs.npm_token }} @@ -276,54 +186,50 @@ runs: echo "🏗️ Running npm run build before publish..." npm run build else - echo "ℹ️ No \"build\" script defined, publishing without build step." + echo "❌ No \"build\" script defined in package.json" + echo " Please add a build script (it can be a no-op if your project does not need a build step)." + exit 1 fi echo "🚀 Publishing package to npm with dist-tag '${{ inputs.dist_tag }}'..." npm publish --tag "${{ inputs.dist_tag }}" --registry "${{ inputs.npm_registry }}" - name: Create GitHub Release - if: ${{ steps.determine_release.outputs.should_release == 'true' && inputs.dry_run == 'false' }} + if: ${{ inputs.dry_run == 'false' }} id: create_release uses: softprops/action-gh-release@v1 with: - tag_name: v${{ steps.determine_release.outputs.new_version }} - name: Release v${{ steps.determine_release.outputs.new_version }} + tag_name: ${{ steps.get_version.outputs.tag }} + name: Release ${{ steps.get_version.outputs.tag }} body: | - Automated release for v${{ steps.determine_release.outputs.new_version }}. + ## Release Notes - See CHANGELOG.md for full details. + ${{ steps.get_changelog.outputs.release_notes }} draft: false - prerelease: ${{ contains(steps.determine_release.outputs.new_version, '-') }} + prerelease: ${{ contains(steps.get_version.outputs.version, '-') }} token: ${{ inputs.token }} - name: Dry-run summary (no publish) - if: ${{ steps.determine_release.outputs.should_release == 'true' && inputs.dry_run == 'true' }} + if: ${{ inputs.dry_run == 'true' }} shell: bash run: | - NEW_VERSION="${{ steps.determine_release.outputs.new_version }}" + VERSION="${{ steps.get_version.outputs.version }}" + TAG="${{ steps.get_version.outputs.tag }}" echo "## 🚧 JavaScript Release (dry-run)" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" - echo "- Recommended new version: \`$NEW_VERSION\`" >> "$GITHUB_STEP_SUMMARY" - echo "- No files, tags, or npm publish were performed because dry_run=true" >> "$GITHUB_STEP_SUMMARY" - - - name: No-release summary - if: ${{ steps.determine_release.outputs.should_release != 'true' }} - shell: bash - run: | - echo "## ℹ️ JavaScript Release" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "No conventional-commit changes requiring a release were detected. No new version was published." >> "$GITHUB_STEP_SUMMARY" + echo "- Version: \`$VERSION\`" >> "$GITHUB_STEP_SUMMARY" + echo "- Tag: \`$TAG\`" >> "$GITHUB_STEP_SUMMARY" + echo "- No tags, npm publish, or GitHub Release were performed because dry_run=true" >> "$GITHUB_STEP_SUMMARY" - name: Release summary - if: ${{ steps.determine_release.outputs.should_release == 'true' && inputs.dry_run == 'false' }} + if: ${{ inputs.dry_run == 'false' }} shell: bash run: | - NEW_VERSION="${{ steps.determine_release.outputs.new_version }}" - TAG="v$NEW_VERSION" + VERSION="${{ steps.get_version.outputs.version }}" + TAG="${{ steps.get_version.outputs.tag }}" echo "## 🎉 JavaScript Release Summary" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" - echo "- Released version: \`$NEW_VERSION\`" >> "$GITHUB_STEP_SUMMARY" + echo "- Released version: \`$VERSION\`" >> "$GITHUB_STEP_SUMMARY" echo "- Git tag: \`$TAG\`" >> "$GITHUB_STEP_SUMMARY" echo "- npm dist-tag: \`${{ inputs.dist_tag }}\`" >> "$GITHUB_STEP_SUMMARY" echo "- Branch: \`${{ inputs.release_branch }}\`" >> "$GITHUB_STEP_SUMMARY" From c28bf6b0999612a2312d6cf074f4519d9d70ad19 Mon Sep 17 00:00:00 2001 From: Lantum-Brendan Date: Sat, 20 Dec 2025 14:50:37 +0100 Subject: [PATCH 3/3] docs: Update Readme.md --- README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 032c915..e57acdf 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ workflows/ │ ├── pre-release/ # Pre-release validation │ └── release/ # Create PHP package release ├── javascript/ -│ ├── ci/ # Lint, optional tests, typecheck, build for Node.js/npm libs +│ ├── ci/ # Lint, typecheck, build, and optional tests for Node.js/npm libs │ └── release/ # Publish npm package and create GitHub release ├── package/ # [DEPRECATED] Use php/ instead │ ├── pre-release/ @@ -307,9 +307,13 @@ jobs: **Requirements:** 1. `package.json` with a valid `version` field. -2. `build` script in `package.json` (mandatory; can be a no-op if your project does not need a build step). -3. `CHANGELOG.md` (optional but recommended; if present, include an entry like `## [X.Y.Z] - YYYY-MM-DD` for the version). -4. `NPM_TOKEN` secret configured with publish permissions for the target npm registry. +2. The following npm scripts in `package.json` (all mandatory): + - `lint` (or override via `lint_script` input) + - `typecheck` (can be a no-op if not using TypeScript) + - `build` (can be a no-op if no build step is needed) +3. `test` or `test:ci` script (optional, controlled by `run_tests` input) +4. `CHANGELOG.md` (optional but recommended; if present, include an entry like `## [X.Y.Z] - YYYY-MM-DD` for the version). +5. `NPM_TOKEN` secret configured with publish permissions for the target npm registry. ---