Skip to content

ci(github-actions): update update-node-npm.yml workflow to use PR template file #942

@beatrizsmerino

Description

@beatrizsmerino

ci(github-actions): update update-node-npm.yml workflow to use PR template file

⏱️ Estimate 📊 Priority 📏 Size 📅 Start 📅 End
4h P2 M 05-02-2026 05-02-2026

📸 Screenshots

Current Expected
N/A — This change has no visual impact. N/A — This change has no visual impact.

📝 Summary

  • The update-node-npm.yml workflow generates PRs with an outdated body format
  • Extract the PR body to a template file .github/WORKFLOW_TEMPLATE/pr-node-npm.md with placeholders
  • Update the workflow to read the template, replace placeholders and generate the PR body
  • Includes improvements to the job summary output

💡 Why this change?

  • The PR body is currently built inline as a YAML multiline string, making it hard to read and maintain
  • A separate template file is easier to edit, review and version control
  • The current format uses outdated headers (⏱️ Time, 📅 Date, 🔧 Build Type, 📝 Description)
  • Missing sections: 📸 Screenshots, 📌 Notes, structured 🔗 References
  • Tests should not be pre-checked [x] since the workflow does not verify them
  • Branch name uses dependabot/ prefix which is misleading
  • The Changes Made section should use Update/Keep logic based on actual changes

📋 Steps

Phase 1: Create PR template file

  • Create .github/WORKFLOW_TEMPLATE/pr-node-npm.md with placeholders
# build(deps): update `node@{{LATEST_NODE}}` and `npm@{{LATEST_NPM}}` versions

| ⏱️ Estimate | 📊 Priority | 📏 Size | 📅 Start | 📅 End |
|---------|-------------|---------|----------|--------|
| 2h | P2 | XS | {{DATE}} | {{DATE}} |

## 📸 Screenshots

| Before | After |
|:------:|:-----:|
| N/A — This change has no visual impact. | N/A — This change has no visual impact. |

## 🔄 Type of Change
- [ ] Bug fix
- [ ] Breaking change
- [x] Dependency
- [ ] New feature
- [ ] Improvement
- [x] Configuration
- [ ] Documentation
- [x] CI/CD

## 📝 Summary

- Automatic update of `node` and `npm` versions detected by the `update-node-npm.yml` workflow
- Runs monthly (first Saturday) or manually via `workflow_dispatch`
- Checks for new Node.js LTS versions and creates this PR to update the project configuration files
- Updates `package.json` `engines` (`node` and `npm`) to the bundled versions
- If `.noderc.json` exists with `maxMajorVersion`, updates only within that major version

## 📋 Changes Made

{{CHANGES_MADE}}

## 🧪 Tests

- [ ] Verify `node -v` matches the updated version in `.nvmrc`
- [ ] Verify `npm -v` matches the updated version in `package.json` `engines.npm`
- [ ] Run `npm install` without errors
- [ ] Run `npm run build` without errors
- [ ] Run `npm run dev` and check the app loads correctly
- [ ] Check CI workflows pass on the PR branch

## 📌 Notes

Version changes:

| File | Configuration | From | To | Type |
|------|---------------|------|-----|------|
{{NOTES_TABLE}}

## 🔗 References

### Files to modify
- `.nvmrc`
- `package.json`
- `.github/workflows/check-node.yml`

### Documentation
- [Node.js — Release schedule](https://nodejs.org/en/about/releases/)
- [Node.js — Changelog](https://github.com/nodejs/node/blob/main/CHANGELOG.md)
- [NPM CLI — Releases](https://github.com/npm/cli/releases)
- [NVM — Node Version Manager](https://github.com/nvm-sh/nvm)
- [GitHub Actions — `setup-node` matrix testing](https://github.com/actions/setup-node?tab=readme-ov-file#matrix-testing)

### Related Files
- `.github/workflows/update-node-npm.yml`

Phase 2: Add change detection step

  • Add step after 🔄 Check if update is needed to detect change types and version types
- name: 🔍 Detect change types
  if: steps.check-update.outputs.update_needed == 'true'
  id: changes
  run: |
    get_change_type() {
      local old=$1 new=$2
      local old_major=$(echo "$old" | tr -d 'v[]' | cut -d. -f1 | tr -d ' ')
      local old_minor=$(echo "$old" | tr -d 'v[]' | cut -d. -f2 | tr -d ' ')
      local new_major=$(echo "$new" | tr -d 'v[]' | cut -d. -f1 | tr -d ' ')
      local new_minor=$(echo "$new" | tr -d 'v[]' | cut -d. -f2 | tr -d ' ')
      if [ "$old_major" != "$new_major" ]; then echo "🔴 Major"
      elif [ "$old_minor" != "$new_minor" ]; then echo "🟡 Minor"
      else echo "🟢 Patch"; fi
    }

    CURRENT_NODE="${{ steps.current-versions.outputs.current_node }}"
    LATEST_NODE="${{ steps.node-versions.outputs.latest_version }}"
    CURRENT_NPM="${{ steps.current-versions.outputs.current_npm }}"
    LATEST_NPM="${{ steps.node-versions.outputs.npm_version }}"
    CURRENT_MATRIX="${{ steps.current-versions.outputs.current_matrix }}"
    NEW_MATRIX="${{ steps.node-versions.outputs.matrix }}"

    echo "node_type=$(get_change_type "$CURRENT_NODE" "$LATEST_NODE")" >> $GITHUB_OUTPUT

    if [ "$CURRENT_NPM" != "$LATEST_NPM" ]; then
      echo "npm_changed=true" >> $GITHUB_OUTPUT
      echo "npm_type=$(get_change_type "$CURRENT_NPM" "$LATEST_NPM")" >> $GITHUB_OUTPUT
    else
      echo "npm_changed=false" >> $GITHUB_OUTPUT
      echo "npm_type=-" >> $GITHUB_OUTPUT
    fi

    if [ "$CURRENT_MATRIX" != "$NEW_MATRIX" ]; then
      echo "matrix_changed=true" >> $GITHUB_OUTPUT
      echo "matrix_type=🔴 Major" >> $GITHUB_OUTPUT
    else
      echo "matrix_changed=false" >> $GITHUB_OUTPUT
      echo "matrix_type=-" >> $GITHUB_OUTPUT
    fi

Phase 3: Build PR body from template

  • Add step to generate dynamic sections and replace placeholders in template
- name: 📄 Build PR body
  if: steps.check-update.outputs.update_needed == 'true'
  run: |
    CURRENT_NODE="${{ steps.current-versions.outputs.current_node }}"
    LATEST_NODE="${{ steps.node-versions.outputs.latest_version }}"
    CURRENT_NPM="${{ steps.current-versions.outputs.current_npm }}"
    LATEST_NPM="${{ steps.node-versions.outputs.npm_version }}"
    CURRENT_MATRIX="${{ steps.current-versions.outputs.current_matrix }}"
    NEW_MATRIX="${{ steps.node-versions.outputs.matrix }}"
    DATE="${{ steps.check-update.outputs.date }}"
    NODE_TYPE="${{ steps.changes.outputs.node_type }}"
    NPM_TYPE="${{ steps.changes.outputs.npm_type }}"
    MATRIX_TYPE="${{ steps.changes.outputs.matrix_type }}"

    # Build Changes Made
    CHANGES="- Update \`.nvmrc\` file: \`v${CURRENT_NODE}\` → \`v${LATEST_NODE}\`
"
    CHANGES+="- Update \`package.json\` file in \`engines.node\`: \`${CURRENT_NODE}\` → \`${LATEST_NODE}\`
"
    if [ "${{ steps.changes.outputs.npm_changed }}" == "true" ]; then
      CHANGES+="- Update \`package.json\` file in \`engines.npm\`: \`${CURRENT_NPM}\` → \`${LATEST_NPM}\`
"
    else
      CHANGES+="- Keep \`package.json\` file in \`engines.npm\`: \`${CURRENT_NPM}\`
"
    fi
    if [ "${{ steps.changes.outputs.matrix_changed }}" == "true" ]; then
      CHANGES+="- Update \`check-node.yml\` file in matrix: \`${CURRENT_MATRIX}\` → \`${NEW_MATRIX}\`"
    else
      CHANGES+="- Keep \`check-node.yml\` file in matrix: \`${CURRENT_MATRIX}\`"
    fi

    # Build Notes table rows
    NOTES="| \`.nvmrc\` | Node version | \`v${CURRENT_NODE}\` | \`v${LATEST_NODE}\` | ${NODE_TYPE} |
"
    NOTES+="| \`package.json\` | engines.node | \`${CURRENT_NODE}\` | \`${LATEST_NODE}\` | ${NODE_TYPE} |
"
    if [ "${{ steps.changes.outputs.npm_changed }}" == "true" ]; then
      NOTES+="| \`package.json\` | engines.npm | \`${CURRENT_NPM}\` | \`${LATEST_NPM}\` | ${NPM_TYPE} |
"
    else
      NOTES+="| \`package.json\` | engines.npm | \`${CURRENT_NPM}\` | \`-\` | \`-\` |
"
    fi
    if [ "${{ steps.changes.outputs.matrix_changed }}" == "true" ]; then
      NOTES+="| \`check-node.yml\` | matrix | \`${CURRENT_MATRIX}\` | \`${NEW_MATRIX}\` | ${MATRIX_TYPE} |"
    else
      NOTES+="| \`check-node.yml\` | matrix | \`${CURRENT_MATRIX}\` | \`-\` | \`-\` |"
    fi

    # Write dynamic sections to temp files
    echo -e "$CHANGES" > /tmp/changes.txt
    echo -e "$NOTES" > /tmp/notes.txt

    # Copy template and replace simple placeholders
    cp .github/WORKFLOW_TEMPLATE/pr-node-npm.md pr-body.md
    sed -i "s|{{LATEST_NODE}}|${LATEST_NODE}|g" pr-body.md
    sed -i "s|{{LATEST_NPM}}|${LATEST_NPM}|g" pr-body.md
    sed -i "s|{{DATE}}|${DATE}|g" pr-body.md

    # Replace multi-line placeholders
    awk '/{{CHANGES_MADE}}/{system("cat /tmp/changes.txt"); next}1' pr-body.md > pr-body.tmp && mv pr-body.tmp pr-body.md
    awk '/{{NOTES_TABLE}}/{system("cat /tmp/notes.txt"); next}1' pr-body.md > pr-body.tmp && mv pr-body.tmp pr-body.md

Phase 4: Update create-pull-request step

  • Update title, commit message, branch, labels and use body-path
- name: 🚀 Create Pull Request
  if: |
    steps.check-update.outputs.update_needed == 'true' &&
    (steps.current-versions.outputs.has_nvmrc == 'true' ||
     steps.current-versions.outputs.has_engines == 'true' ||
     steps.current-versions.outputs.has_node_yml == 'true')
  id: create-pr
  uses: peter-evans/create-pull-request@v8
  with:
    token: ${{ secrets.PAT_WORKFLOW }}
    commit-message: "build(deps): update `node@${{ steps.node-versions.outputs.latest_version }}` and `npm@${{ steps.node-versions.outputs.npm_version }}` versions"
    title: "build(deps): update `node@${{ steps.node-versions.outputs.latest_version }}` and `npm@${{ steps.node-versions.outputs.npm_version }}` versions"
    body-path: pr-body.md
    branch: update/node-${{ steps.node-versions.outputs.latest_version }}
    delete-branch: true
    labels: |
      dependencies
      github_actions
      configuration
    assignees: beatrizsmerino

Phase 5: Update job summary

  • Replace the 📊 Summary step with the new format
- name: 📊 Summary
  run: |
    if [ "${{ steps.check-update.outputs.update_needed }}" == "true" ]; then
      STATUS_BADGE="![Update](https://img.shields.io/badge/Status-Update%20Available-yellow)"
    else
      STATUS_BADGE="![Up to date](https://img.shields.io/badge/Status-Up%20to%20Date-green)"
    fi

    if [ "${{ steps.config.outputs.has_config }}" == "true" ]; then
      LIMIT_BADGE="![Limited](https://img.shields.io/badge/Max%20Version-v${{ steps.config.outputs.max_major }}-blue)"
      MAX_VALUE="\`${{ steps.config.outputs.max_major }}\`"
    else
      LIMIT_BADGE="![No Limit](https://img.shields.io/badge/Max%20Version-None-gray)"
      MAX_VALUE="\`-\`"
    fi

    NODE_TYPE="${{ steps.changes.outputs.node_type }}"
    NPM_TYPE="${{ steps.changes.outputs.npm_type }}"
    MATRIX_TYPE="${{ steps.changes.outputs.matrix_type }}"
    if [ "${{ steps.check-update.outputs.update_needed }}" != "true" ]; then
      NODE_TYPE="-"; NPM_TYPE="-"; MATRIX_TYPE="-"
    fi

    NVMRC_STATUS=$( [ "${{ steps.current-versions.outputs.has_nvmrc }}" == "true" ] && echo "✅ Found" || echo "❌ Not found" )
    ENGINES_STATUS=$( [ "${{ steps.current-versions.outputs.has_engines }}" == "true" ] && echo "✅ Found" || echo "❌ Not found" )
    NODE_YML_STATUS=$( [ "${{ steps.current-versions.outputs.has_node_yml }}" == "true" ] && echo "✅ Found" || echo "❌ Not found" )
    NODERC_STATUS=$( [ "${{ steps.config.outputs.has_config }}" == "true" ] && echo "✅ Found" || echo "❌ Not found" )

    PR_URL="${{ steps.create-pr.outputs.pull-request-url }}"
    PR_NUMBER="${{ steps.create-pr.outputs.pull-request-number }}"
    if [ -n "$PR_URL" ]; then
      PR_LINK="✅ [PR #${PR_NUMBER} — build(deps): update \`node@${{ steps.node-versions.outputs.latest_version }}\` and \`npm@${{ steps.node-versions.outputs.npm_version }}\` versions](${PR_URL})"
    else
      PR_LINK="ℹ️ No update needed — all versions are up to date."
    fi

    cat >> $GITHUB_STEP_SUMMARY << EOF
    # Node.js Update Check Summary

    ${STATUS_BADGE} ${LIMIT_BADGE}

    > 📅 Last check: $(date +%d-%m-%Y)

    ## 🔢 Version Comparison

    | Package | Files | Max | Current | Latest | Type |
    |---------|-------|:---:|:-------:|:------:|:----:|
    | **Node** | \`.nvmrc\`, \`package.json\` | ${MAX_VALUE} | \`${{ steps.current-versions.outputs.current_node }}\` | \`${{ steps.node-versions.outputs.latest_version }}\` | ${NODE_TYPE} |
    | **NPM** | \`package.json\` | \`-\` | \`${{ steps.current-versions.outputs.current_npm }}\` | \`${{ steps.node-versions.outputs.npm_version }}\` | ${NPM_TYPE} |
    | **CI Matrix** | \`check-node.yml\` | \`-\` | \`${{ steps.current-versions.outputs.current_matrix }}\` | \`${{ steps.node-versions.outputs.matrix }}\` | ${MATRIX_TYPE} |

    > **Max** is read from \`.noderc.json\` (\`maxMajorVersion\` field). If the file doesn't exist, updates to the latest LTS version.

    ## 📁 Project Files

    | File | Status |
    |------|:------:|
    | \`.noderc.json\` | ${NODERC_STATUS} |
    | \`.nvmrc\` | ${NVMRC_STATUS} |
    | \`package.json\` | ${ENGINES_STATUS} |
    | \`check-node.yml\` | ${NODE_YML_STATUS} |

    ## 🔗 Pull Request

    ${PR_LINK}
    EOF

🧪 Tests

  • Trigger workflow manually with workflow_dispatch
  • Verify PR is created with the new body format from template
  • Verify PR title uses build(deps): prefix
  • Verify PR branch uses update/node- prefix instead of dependabot/
  • Verify PR labels include dependencies, github_actions and configuration
  • Verify Changes Made section uses Update/Keep correctly
  • Verify Notes table shows correct version types (🟢 Patch / 🟡 Minor / 🔴 Major)
  • Verify job summary shows Version Comparison table with Type and Max columns
  • Verify job summary shows PR link when created
  • Verify job summary shows "No update needed" when up to date

🔗 References

Files to create

  • .github/WORKFLOW_TEMPLATE/pr-node-npm.md

Files to modify

  • .github/workflows/update-node-npm.yml

Related Issues

Metadata

Metadata

Labels

configurationProject setup and configuration filesdependenciesDependency updatesgithub_actionsGitHub .github/ folder configuration

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions