Skip to content
36 changes: 32 additions & 4 deletions actions/release-tag-creation/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ inputs:
maintain_email:
required: true
description: 'Git user.email for the version-bump commit.'
plugin:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

And if the value is "koko"?

required: false
default: 'core'
description: 'Plugin being released: "core" (bumps elementor.php and readme.txt) or "pro" (bumps elementor-pro.php and sets "Elementor tested up to" from Core release/<channel>).'
github_token:
required: false
default: ${{ github.token }}
description: 'GitHub token for fetching Core release version (optional, improves rate limits and avoids CDN cache).'

outputs:
channel:
Expand All @@ -32,6 +40,9 @@ outputs:
readme_beta_tag:
description: 'The Beta tag value written to readme.txt.'
value: ${{ steps.capture-readme.outputs.readme_beta_tag }}
elementor_tested_up_to:
description: 'The "Elementor tested up to" value written to the plugin file. Empty when not updated.'
value: ${{ steps.capture-readme.outputs.elementor_tested_up_to }}

runs:
using: composite
Expand Down Expand Up @@ -77,20 +88,28 @@ runs:
INPUT_VERSION: ${{ inputs.version }}
INPUT_CHANNEL: ${{ steps.handle-version-input.outputs.channel }}
INPUT_COMPANION_TAG: ${{ steps.handle-version-input.outputs.companion_tag }}
INPUT_PLUGIN: ${{ inputs.plugin }}
INPUT_GITHUB_TOKEN: ${{ inputs.github_token }}
ACTION_PATH: ${{ github.action_path }}
run: node "$ACTION_PATH/update-version-files.ts"
run: node "$ACTION_PATH/run-update-version-files.ts"

- name: Commit version bump
shell: bash
env:
VERSION: ${{ inputs.version }}
DRY_RUN: ${{ inputs.dry_run }}
PLUGIN_FILE: ${{ steps.capture-readme.outputs.plugin_file }}
UPDATE_README: ${{ steps.capture-readme.outputs.update_readme }}
run: |
if [[ "${DRY_RUN}" == "true" ]]; then
echo "Dry run — skipping commit."
exit 0
fi
git add elementor.php readme.txt
FILES_TO_COMMIT=("${PLUGIN_FILE}")
if [[ "${UPDATE_README}" == "true" ]]; then
FILES_TO_COMMIT+=("readme.txt")
fi
git add "${FILES_TO_COMMIT[@]}"
if git diff --cached --quiet; then
echo "No changes to commit — version files already at ${VERSION}."
exit 0
Expand Down Expand Up @@ -124,6 +143,9 @@ runs:
BRANCH: ${{ steps.handle-version-input.outputs.checkout_branch }}
STABLE_TAG: ${{ steps.capture-readme.outputs.readme_stable_tag }}
BETA_TAG: ${{ steps.capture-readme.outputs.readme_beta_tag }}
TESTED_UP_TO: ${{ steps.capture-readme.outputs.elementor_tested_up_to }}
PLUGIN_FILE: ${{ steps.capture-readme.outputs.plugin_file }}
UPDATE_README: ${{ steps.capture-readme.outputs.update_readme }}
VERSION: ${{ inputs.version }}
run: |
if [[ "${DRY_RUN}" == "true" ]]; then
Expand All @@ -148,6 +170,12 @@ runs:
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}\` |"
echo "| **Plugin file** | \`${PLUGIN_FILE}\` |"
if [[ "${UPDATE_README}" == "true" ]]; then
echo "| **readme.txt stable tag** | \`${STABLE_TAG}\` |"
echo "| **readme.txt beta tag** | \`${BETA_TAG}\` |"
fi
if [[ -n "${TESTED_UP_TO}" ]]; then
echo "| **Elementor tested up to** | \`${TESTED_UP_TO}\` |"
fi
} >> "$GITHUB_STEP_SUMMARY"
94 changes: 94 additions & 0 deletions actions/release-tag-creation/elementor-tested-up-to.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { afterEach, describe, expect, it, vi } from 'vitest';

import {
fetchCoreReleaseVersion,
toTestedUpToVersion,
} from './elementor-tested-up-to.ts';

const HTTP_OK = 200;
const HTTP_NOT_FOUND = 404;

function mockFetchResponse(body: string, status = HTTP_OK) {
const fetchMock = vi.fn().mockResolvedValue(new Response(body, { status }));
vi.stubGlobal('fetch', fetchMock);
return fetchMock;
}

describe('toTestedUpToVersion', () => {
it.each([
['4.3.0', '4.3.0'],
['4.4.0-beta2', '4.4.0'],
['4.10.12', '4.10.12'],
])('%s → %s', (coreVersion, expected) => {
expect(toTestedUpToVersion(coreVersion)).toBe(expected);
});

it('throws on an invalid version', () => {
expect(() => toTestedUpToVersion('4.3')).toThrow('not valid semver');
});
});

describe('fetchCoreReleaseVersion', () => {
afterEach(() => {
vi.unstubAllGlobals();
});

it.each(['stable', 'beta'] as const)(
'reads Version from Core release/%s via GitHub Contents API',
async (channel) => {
const fetchMock = mockFetchResponse(
['<?php', '/**', ' * Version: 4.4.0-beta2', ' */'].join('\n'),
);

await expect(fetchCoreReleaseVersion(channel)).resolves.toBe(
'4.4.0-beta2',
);
expect(fetchMock).toHaveBeenCalledWith(
`https://api.github.com/repos/elementor/elementor/contents/elementor.php?ref=release/${channel}`,
{
headers: {
Accept: 'application/vnd.github.raw',
'User-Agent': 'elementor-editor-github-actions',
},
},
);
},
);

it('includes Authorization header when token is provided', async () => {
const fetchMock = mockFetchResponse(
['<?php', '/**', ' * Version: 4.4.0-beta2', ' */'].join('\n'),
);
const token = 'ghp_test123';

await expect(fetchCoreReleaseVersion('stable', token)).resolves.toBe(
'4.4.0-beta2',
);
expect(fetchMock).toHaveBeenCalledWith(
'https://api.github.com/repos/elementor/elementor/contents/elementor.php?ref=release/stable',
{
headers: {
Accept: 'application/vnd.github.raw',
Authorization: 'Bearer ghp_test123',
'User-Agent': 'elementor-editor-github-actions',
},
},
);
});

it('throws when the request fails', async () => {
mockFetchResponse('Not Found', HTTP_NOT_FOUND);

await expect(fetchCoreReleaseVersion('beta')).rejects.toThrow(
'HTTP 404',
);
});

it('throws when the Version header is missing', async () => {
mockFetchResponse('<?php // no header');

await expect(fetchCoreReleaseVersion('stable')).rejects.toThrow(
'No "Version:" header found',
);
});
});
47 changes: 47 additions & 0 deletions actions/release-tag-creation/elementor-tested-up-to.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import semver from 'semver';
import { parseHeaderField } from '@elementor/editor-github-actions-utils';

export type ReleaseChannel = 'stable' | 'beta';

export function toTestedUpToVersion(coreVersion: string): string {
const parsed = semver.parse(coreVersion);

if (!parsed) {
throw new Error(`Core version "${coreVersion}" is not valid semver.`);
}

return `${String(parsed.major)}.${String(parsed.minor)}.${String(parsed.patch)}`;
}

export async function fetchCoreReleaseVersion(
channel: ReleaseChannel,
token?: string,
): Promise<string> {
const ref = `release/${channel}`;
const url = `https://api.github.com/repos/elementor/elementor/contents/elementor.php?ref=${ref}`;

const headers: HeadersInit = {
Accept: 'application/vnd.github.raw',
'User-Agent': 'elementor-editor-github-actions',
};

if (token) {
headers['Authorization'] = `Bearer ${token}`;
}

const response = await fetch(url, { headers });

if (!response.ok) {
throw new Error(
`Failed to fetch Core plugin file from ${url}: HTTP ${String(response.status)}`,
);
}

const version = parseHeaderField(await response.text(), 'Version');

if (!version) {
throw new Error(`No "Version:" header found in ${url}`);
}

return version;
}
29 changes: 29 additions & 0 deletions actions/release-tag-creation/plugin-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, it } from 'vitest';

import { resolvePluginConfig } from './plugin-config.ts';

describe('resolvePluginConfig', () => {
it('resolves Core to elementor.php with readme updates', () => {
expect(resolvePluginConfig('core')).toEqual({
pluginFile: 'elementor.php',
versionConstant: 'ELEMENTOR_VERSION',
updateReadme: true,
updateElementorTestedUpTo: false,
});
});

it('resolves Pro to elementor-pro.php with Elementor tested up to updates', () => {
expect(resolvePluginConfig('pro')).toEqual({
pluginFile: 'elementor-pro.php',
versionConstant: 'ELEMENTOR_PRO_VERSION',
updateReadme: false,
updateElementorTestedUpTo: true,
});
});

it.each(['', 'Core', 'elementor'])('throws on "%s"', (plugin) => {
expect(() => resolvePluginConfig(plugin)).toThrow(
'plugin must be "core" or "pro"',
);
});
});
32 changes: 32 additions & 0 deletions actions/release-tag-creation/plugin-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { VersionConstant } from '@elementor/editor-github-actions-utils';

export type Plugin = 'core' | 'pro';

export type PluginConfig = {
pluginFile: string;
versionConstant: VersionConstant;
updateReadme: boolean;
updateElementorTestedUpTo: boolean;
};

const PLUGIN_CONFIGS: Record<Plugin, PluginConfig> = {
core: {
pluginFile: 'elementor.php',
versionConstant: 'ELEMENTOR_VERSION',
updateReadme: true,
updateElementorTestedUpTo: false,
},
pro: {
pluginFile: 'elementor-pro.php',
versionConstant: 'ELEMENTOR_PRO_VERSION',
updateReadme: false,
updateElementorTestedUpTo: true,
},
};

export function resolvePluginConfig(plugin: string): PluginConfig {
if (plugin !== 'core' && plugin !== 'pro') {
throw new Error(`plugin must be "core" or "pro", got "${plugin}"`);
}
return PLUGIN_CONFIGS[plugin];
}
3 changes: 3 additions & 0 deletions actions/release-tag-creation/run-update-version-files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { run } from './update-version-files.ts';

void run();
Loading
Loading