Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
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.

- 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.
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.

- 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.
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

- 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.

- 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.
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.
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.
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
git push origin "${{ inputs.version }}"

Check warning

Code scanning / CodeQL

Code injection Medium

Potential code injection in
${ inputs.version }
, which may be controlled by an external user.
echo "Release tag ${{ inputs.version }} pushed successfully (branch unchanged)."

Check warning

Code scanning / CodeQL

Code injection Medium

Potential code injection in
${ inputs.version }
, which may be controlled by an external user.
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.
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.
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.
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.
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.
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.

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.
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
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