Skip to content
Draft
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
ce4be27
feat: two step release tag creation [ED-24451]
davseve Jun 24, 2026
5a9ba70
build: add compiled dist for release-tag-creation action
davseve Jun 24, 2026
293e617
add tests
davseve Jun 24, 2026
1da6c94
build
davseve Jun 24, 2026
3e0375c
add tests infra
davseve Jun 24, 2026
b6f19a4
lint
davseve Jun 24, 2026
40d6b85
add tests to version re-write
davseve Jun 24, 2026
6eb0903
wip
davseve Jun 24, 2026
b5432e7
summery report
davseve Jun 24, 2026
8a5cb68
wip
davseve Jun 25, 2026
6619d47
Remove v from tag version
davseve Jun 25, 2026
11ce058
fix test
davseve Jun 25, 2026
4f7dc35
change branch model
davseve Jul 5, 2026
351ad86
wip
davseve Jul 5, 2026
d8a80b9
check current version is the valid next version
davseve Jul 8, 2026
270cba7
check that the next version is the correct one
davseve Jul 8, 2026
b091e15
test coverage for version gap validation
davseve Jul 8, 2026
929fb09
remove ts bundle
davseve Jul 8, 2026
621a0ea
Revert "remove ts bundle"
davseve Jul 8, 2026
beac187
remove ts to js bundle
davseve Jul 8, 2026
e2965d3
update package lock
davseve Jul 8, 2026
5903c3e
fix: use npm install instead of npm ci for composite action
davseve Jul 8, 2026
63e0778
fix: inline utils functions to make action self-contained
davseve Jul 8, 2026
0112aa5
fix: call run() in main.ts and set ESM module type
davseve Jul 8, 2026
c9c8f75
Netanetl's review comments
davseve Jul 8, 2026
dedb58f
fix: add .ts extensions to relative imports for Node ESM resolution
davseve Jul 8, 2026
08b73ee
allow extension
davseve Jul 8, 2026
09c77f0
wip
davseve Jul 8, 2026
bcdc0fd
wip
davseve Jul 8, 2026
433a5b9
fix: address CodeQL code injection and command injection findings
davseve Jul 9, 2026
f4c8adb
remove code duplication
davseve Jul 9, 2026
5daee0f
wip
davseve Jul 9, 2026
39d20b6
lint
davseve Jul 9, 2026
5444eeb
fix: resolve lint failures and activate Elementor in test flow
cursoragent Jul 9, 2026
98007cd
fix: use WordPress 6.8 in performance flow test
cursoragent Jul 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,20 @@
- name: Run Lint
run: npm run lint

test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v4

- name: Install Dependencies

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure it uses the correct node version

uses: ./.github/actions/install-deps

- name: Run Tests
run: npm test

require-build:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
name: Require Build in PR
runs-on: ubuntu-latest
steps:
Expand Down
131 changes: 131 additions & 0 deletions actions/release-tag-creation/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
name: Release Tag Creation
description: Validates the version, bumps version files, creates and pushes a git release tag.

inputs:
version:
required: true
description: 'Version to release (e.g. 4.1.0-beta1 or 4.0.11).'
dry_run:
required: false
default: 'true'
description: 'Dry run — validate everything but do not push commits or create tag.'
maintain_username:
required: true
description: 'Git user.name for the version-bump commit.'
maintain_email:
required: true
description: 'Git user.email for the version-bump commit.'

outputs:
channel:
description: 'The release channel: stable or beta.'
value: ${{ steps.handle-version-input.outputs.channel }}
checkout_branch:
description: 'The branch the version bump was applied to.'
value: ${{ steps.handle-version-input.outputs.checkout_branch }}
tag_pushed:
description: 'Whether the tag was pushed (true/false).'
value: ${{ steps.create-release-tag.outputs.tag_pushed }}
readme_stable_tag:
description: 'The Stable tag value written to readme.txt.'
value: ${{ steps.capture-readme.outputs.readme_stable_tag }}
readme_beta_tag:
description: 'The Beta tag value written to readme.txt.'
value: ${{ steps.capture-readme.outputs.readme_beta_tag }}

runs:
using: composite
steps:
- name: Handle version input
id: handle-version-input
shell: bash
env:
INPUT_VERSION: ${{ inputs.version }}
run: node "${{ github.action_path }}/dist/index.js"

Check warning

Code scanning / CodeQL

Code injection Medium

Potential code injection in
${ github.action_path }
, which may be controlled by an external user.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
davseve marked this conversation as resolved.
Outdated

- name: Switch to release branch
shell: bash
run: |
git checkout ${{ steps.handle-version-input.outputs.checkout_branch }}

Check warning

Code scanning / CodeQL

Code injection Medium

Potential code injection in
${ steps.handle-version-input.outputs.checkout_branch }
, which may be controlled by an external user.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
git checkout ${{ github.sha }} -- .github/

Check warning

Code scanning / CodeQL

Code injection Medium

Potential code injection in
${ github.sha }
, which may be controlled by an external user.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
davseve marked this conversation as resolved.
Outdated

- name: Config git user
shell: bash
run: |
git config --global user.name "${{ inputs.maintain_username }}"

Check warning

Code scanning / CodeQL

Code injection Medium

Potential code injection in
${ inputs.maintain_username }
, which may be controlled by an external user.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
git config --global user.email "${{ inputs.maintain_email }}"

Check warning

Code scanning / CodeQL

Code injection Medium

Potential code injection in
${ inputs.maintain_email }
, which may be controlled by an external user.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
davseve marked this conversation as resolved.
Outdated

- name: Update version files
id: capture-readme
shell: bash
env:
INPUT_VERSION: ${{ inputs.version }}
INPUT_CHANNEL: ${{ steps.handle-version-input.outputs.channel }}
run: node "${{ github.action_path }}/dist/update-version-files.js"

Check warning

Code scanning / CodeQL

Code injection Medium

Potential code injection in
${ github.action_path }
, which may be controlled by an external user.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed

- name: Commit version bump
shell: bash
run: |
VERSION="${{ inputs.version }}"

Check warning

Code scanning / CodeQL

Code injection Medium

Potential code injection in
${ inputs.version }
, which may be controlled by an external user.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
DRY_RUN="${{ inputs.dry_run }}"

Check warning

Code scanning / CodeQL

Code injection Medium

Potential code injection in
${ inputs.dry_run }
, which may be controlled by an external user.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
if [[ "${DRY_RUN}" == "true" ]]; then
echo "Dry run — skipping commit."
exit 0
fi
git add elementor.php readme.txt
if git diff --cached --quiet; then
echo "No changes to commit — version files already at ${VERSION}."
exit 0
fi
git commit -m "Bump version to ${VERSION}"
echo "Committed version bump to ${VERSION}."

- name: Create release tag
id: create-release-tag
shell: bash
run: |
if [[ "${{ inputs.dry_run }}" == "true" ]]; then

Check warning

Code scanning / CodeQL

Code injection Medium

Potential code injection in
${ inputs.dry_run }
, which may be controlled by an external user.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
echo "Dry run — skipping tag push."
echo "tag_pushed=false" >> "$GITHUB_OUTPUT"
else
git tag -a "${{ inputs.version }}" -m "Release version ${{ inputs.version }}"

Check warning

Code scanning / CodeQL

Code injection Medium

Potential code injection in
${ inputs.version }
, which may be controlled by an external user.

Check warning

Code scanning / CodeQL

Code injection Medium

Potential code injection in
${ inputs.version }
, which may be controlled by an external user.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
git push origin "v${{ inputs.version }}"

Check warning

Code scanning / CodeQL

Code injection Medium

Potential code injection in
${ inputs.version }
, which may be controlled by an external user.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
echo "Release tag v${{ inputs.version }} pushed successfully (branch unchanged)."
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
echo "tag_pushed=true" >> "$GITHUB_OUTPUT"
fi

- name: Write job summary
shell: bash
run: |
DRY_RUN="${{ inputs.dry_run }}"

Check warning

Code scanning / CodeQL

Code injection Medium

Potential code injection in
${ inputs.dry_run }
, which may be controlled by an external user.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
TAG_PUSHED="${{ steps.create-release-tag.outputs.tag_pushed }}"

Check warning

Code scanning / CodeQL

Code injection Medium

Potential code injection in
${ steps.create-release-tag.outputs.tag_pushed }
, which may be controlled by an external user.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
CHANNEL="${{ steps.handle-version-input.outputs.channel }}"

Check warning

Code scanning / CodeQL

Code injection Medium

Potential code injection in
${ steps.handle-version-input.outputs.channel }
, which may be controlled by an external user.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
BRANCH="${{ steps.handle-version-input.outputs.checkout_branch }}"

Check warning

Code scanning / CodeQL

Code injection Medium

Potential code injection in
${ steps.handle-version-input.outputs.checkout_branch }
, which may be controlled by an external user.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
STABLE_TAG="${{ steps.capture-readme.outputs.readme_stable_tag }}"

Check warning

Code scanning / CodeQL

Code injection Medium

Potential code injection in
${ steps.capture-readme.outputs.readme_stable_tag }
, which may be controlled by an external user.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
BETA_TAG="${{ steps.capture-readme.outputs.readme_beta_tag }}"

Check warning

Code scanning / CodeQL

Code injection Medium

Potential code injection in
${ steps.capture-readme.outputs.readme_beta_tag }
, which may be controlled by an external user.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed

if [[ "${DRY_RUN}" == "true" ]]; then
MODE_BADGE="🔵 Dry Run"
else
MODE_BADGE="🚀 Live"
fi

if [[ "${TAG_PUSHED}" == "true" ]]; then
TAG_STATUS="✅ Pushed"
else
TAG_STATUS="⏭️ Skipped (dry run)"
fi

{
echo "## Release Tag Creation Report"
echo ""
echo "| Field | Value |"
echo "| --- | --- |"
echo "| **Mode** | ${MODE_BADGE} |"
echo "| **Version** | \`${{ inputs.version }}\` |"

Check warning

Code scanning / CodeQL

Code injection Medium

Potential code injection in
${ inputs.version }
, which may be controlled by an external user.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
echo "| **Channel** | ${CHANNEL} |"
echo "| **Branch** | \`${BRANCH}\` |"
echo "| **Tag pushed** | ${TAG_STATUS} |"
echo "| **readme.txt stable tag** | \`${STABLE_TAG}\` |"
echo "| **readme.txt beta tag** | \`${BETA_TAG}\` |"
} >> "$GITHUB_STEP_SUMMARY"
6 changes: 6 additions & 0 deletions actions/release-tag-creation/dist/index.js

Large diffs are not rendered by default.

73 changes: 73 additions & 0 deletions actions/release-tag-creation/dist/update-version-files.js

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions actions/release-tag-creation/index.ts
Comment thread
davseve marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { run } from './main';

run();
158 changes: 158 additions & 0 deletions actions/release-tag-creation/main.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { execSync } from 'node:child_process';
import { appendFileSync } from 'node:fs';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import {
checkTagDoesNotExist,
deriveBranch,
extractChannel,
getVersion,
setOutput,
validateFormat,
} from './main';

vi.mock('node:child_process');
vi.mock('node:fs');

const mockExecSync = vi.mocked(execSync);
const mockAppendFileSync = vi.mocked(appendFileSync);

// ─── getVersion ───────────────────────────────────────────────────────────────

describe('getVersion', () => {
afterEach(() => {
delete process.env['INPUT_VERSION'];
});

it('returns the trimmed version from INPUT_VERSION', () => {
process.env['INPUT_VERSION'] = ' 3.11.0 ';
expect(getVersion()).toBe('3.11.0');
});

it('throws when INPUT_VERSION is not set', () => {
delete process.env['INPUT_VERSION'];
expect(() => getVersion()).toThrow('No version provided');
});

it('throws when INPUT_VERSION is an empty string', () => {
process.env['INPUT_VERSION'] = ' ';
expect(() => getVersion()).toThrow('No version provided');
});
});

// ─── setOutput ────────────────────────────────────────────────────────────────

describe('setOutput', () => {
afterEach(() => {
delete process.env['GITHUB_OUTPUT'];
vi.clearAllMocks();
});

it('writes to GITHUB_OUTPUT file when env var is set', () => {
process.env['GITHUB_OUTPUT'] = '/tmp/output';
setOutput('channel', 'stable');
expect(mockAppendFileSync).toHaveBeenCalledWith('/tmp/output', 'channel=stable\n');
});

it('logs to console when GITHUB_OUTPUT is not set', () => {
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
setOutput('channel', 'beta');
expect(consoleSpy).toHaveBeenCalledWith('OUTPUT channel=beta');
consoleSpy.mockRestore();
});
});

// ─── validateFormat ───────────────────────────────────────────────────────────

describe('validateFormat', () => {
it.each([
['4.1.0'],
['5.20.15'],
['4.1.0-beta1'],
['5.20.0-beta3'],
['10.0.0-beta10'],
])('accepts valid version %s', (version) => {
expect(() => { validateFormat(version); }).not.toThrow();
});

it.each([
['v4.1.0'],
['4.1'],
['4.1.0-rc1'],
['4.1.0-beta.1'],
['4.1.0-beta 1'],
['not-a-version'],
[''],
])('rejects invalid version %s', (version) => {
expect(() => { validateFormat(version); }).toThrow('not in the correct format');
});
});

// ─── checkTagDoesNotExist ─────────────────────────────────────────────────────

describe('checkTagDoesNotExist', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('does not throw when ls-remote returns empty (tag absent)', () => {
mockExecSync.mockReturnValue('' as never);
expect(() => checkTagDoesNotExist('3.11.0')).not.toThrow();
});

it('throws when ls-remote returns a line (tag exists)', () => {
mockExecSync.mockReturnValue(
'abc123\trefs/tags/3.11.0\n' as never,
);
expect(() => checkTagDoesNotExist('3.11.0')).toThrow('already exists');
});

it('throws when execSync itself fails', () => {
mockExecSync.mockImplementation(() => {
throw new Error('git: not found');
});
expect(() => checkTagDoesNotExist('3.11.0')).toThrow('Failed to check remote tags');
});

it('queries the exact ref for the given version', () => {
mockExecSync.mockReturnValue('' as never);
checkTagDoesNotExist('4.1.0-beta1');
expect(mockExecSync).toHaveBeenCalledWith(
expect.stringContaining('refs/tags/4.1.0-beta1'),
expect.any(Object),
);
});
});

// ─── extractChannel ───────────────────────────────────────────────────────────

describe('extractChannel', () => {
it('returns "stable" for a plain release version', () => {
expect(extractChannel('4.1.0')).toBe('stable');
});

it('returns "beta" for a beta pre-release', () => {
expect(extractChannel('4.1.0-beta1')).toBe('beta');
});

it('returns "beta" for a higher beta number', () => {
expect(extractChannel('5.20.0-beta3')).toBe('beta');
});

it('throws for an unrecognised pre-release identifier', () => {
expect(() => extractChannel('4.1.0-rc1')).toThrow('Could not determine channel');
});
});

// ─── deriveBranch ─────────────────────────────────────────────────────────────

describe('deriveBranch', () => {
it.each([
['4.1.0', '4.01'],
['5.20.15', '5.20'],
['10.0.0', '10.00'],
['4.1.0-beta1', '4.01'],
])('derives branch %s → %s', (version, expected) => {
expect(deriveBranch(version)).toBe(expected);
});
});
Loading
Loading