-
Notifications
You must be signed in to change notification settings - Fork 5
feat: two step release tag creation [ED-24451] #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
ce4be27
5a9ba70
293e617
1da6c94
3e0375c
b6f19a4
40d6b85
6eb0903
b5432e7
8a5cb68
6619d47
11ce058
4f7dc35
351ad86
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 warningCode scanning / CodeQL Code injection Medium
Potential code injection in
${ github.action_path } Error loading related location Loading |
||
|
davseve marked this conversation as resolved.
|
||
|
|
||
| - name: Switch to release branch | ||
| shell: bash | ||
| run: | | ||
| git checkout ${{ steps.handle-version-input.outputs.checkout_branch }} | ||
Check warningCode scanning / CodeQL Code injection Medium
Potential code injection in
${ steps.handle-version-input.outputs.checkout_branch } Error loading related location Loading |
||
|
|
||
| git checkout ${{ github.sha }} -- .github/ | ||
Check warningCode scanning / CodeQL Code injection Medium
Potential code injection in
${ github.sha } Error loading related location Loading |
||
|
davseve marked this conversation as resolved.
|
||
|
|
||
| - name: Config git user | ||
| shell: bash | ||
| run: | | ||
| git config --global user.name "${{ inputs.maintain_username }}" | ||
Check warningCode scanning / CodeQL Code injection Medium
Potential code injection in
${ inputs.maintain_username } Error loading related location Loading |
||
|
|
||
| git config --global user.email "${{ inputs.maintain_email }}" | ||
Check warningCode scanning / CodeQL Code injection Medium
Potential code injection in
${ inputs.maintain_email } Error loading related location Loading |
||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
davseve marked this conversation as resolved.
|
||
|
|
||
| - 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 warningCode scanning / CodeQL Code injection Medium
Potential code injection in
${ github.action_path } Error loading related location Loading |
||
|
|
||
|
|
||
| - name: Commit version bump | ||
| shell: bash | ||
| run: | | ||
| VERSION="${{ inputs.version }}" | ||
Check warningCode scanning / CodeQL Code injection Medium
Potential code injection in
${ inputs.version } Error loading related location Loading |
||
|
|
||
| DRY_RUN="${{ inputs.dry_run }}" | ||
Check warningCode scanning / CodeQL Code injection Medium
Potential code injection in
${ inputs.dry_run } Error loading related location Loading |
||
|
|
||
| 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 warningCode scanning / CodeQL Code injection Medium
Potential code injection in
${ inputs.dry_run } Error loading related location Loading |
||
|
|
||
| echo "Dry run — skipping tag push." | ||
| echo "tag_pushed=false" >> "$GITHUB_OUTPUT" | ||
| else | ||
| git tag -a "${{ inputs.version }}" -m "Release version ${{ inputs.version }}" | ||
Check warningCode scanning / CodeQL Code injection Medium
Potential code injection in
${ inputs.version } Error loading related location Loading Check warningCode scanning / CodeQL Code injection Medium
Potential code injection in
${ inputs.version } Error loading related location Loading |
||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| git push origin "${{ inputs.version }}" | ||
Check warningCode scanning / CodeQL Code injection Medium
Potential code injection in
${ inputs.version } Error loading related location Loading |
||
|
|
||
| echo "Release tag ${{ inputs.version }} pushed successfully (branch unchanged)." | ||
Check warningCode scanning / CodeQL Code injection Medium
Potential code injection in
${ inputs.version } Error loading related location Loading |
||
|
|
||
| echo "tag_pushed=true" >> "$GITHUB_OUTPUT" | ||
| fi | ||
|
|
||
| - name: Write job summary | ||
| shell: bash | ||
| run: | | ||
| DRY_RUN="${{ inputs.dry_run }}" | ||
Check warningCode scanning / CodeQL Code injection Medium
Potential code injection in
${ inputs.dry_run } Error loading related location Loading |
||
|
|
||
| TAG_PUSHED="${{ steps.create-release-tag.outputs.tag_pushed }}" | ||
Check warningCode scanning / CodeQL Code injection Medium
Potential code injection in
${ steps.create-release-tag.outputs.tag_pushed } Error loading related location Loading |
||
|
|
||
| CHANNEL="${{ steps.handle-version-input.outputs.channel }}" | ||
Check warningCode scanning / CodeQL Code injection Medium
Potential code injection in
${ steps.handle-version-input.outputs.channel } Error loading related location Loading |
||
|
|
||
| BRANCH="${{ steps.handle-version-input.outputs.checkout_branch }}" | ||
Check warningCode scanning / CodeQL Code injection Medium
Potential code injection in
${ steps.handle-version-input.outputs.checkout_branch } Error loading related location Loading |
||
|
|
||
| STABLE_TAG="${{ steps.capture-readme.outputs.readme_stable_tag }}" | ||
Check warningCode scanning / CodeQL Code injection Medium
Potential code injection in
${ steps.capture-readme.outputs.readme_stable_tag } Error loading related location Loading |
||
|
|
||
| BETA_TAG="${{ steps.capture-readme.outputs.readme_beta_tag }}" | ||
Check warningCode scanning / CodeQL Code injection Medium
Potential code injection in
${ steps.capture-readme.outputs.readme_beta_tag } Error loading related location Loading |
||
|
|
||
|
|
||
| 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 warningCode scanning / CodeQL Code injection Medium
Potential code injection in
${ inputs.version } Error loading related location Loading |
||
|
|
||
| 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" | ||
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| import { run } from './main'; | ||
|
|
||
| run(); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| 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', 'release/stable'], | ||
| ['5.20.15', 'release/stable'], | ||
| ['10.0.0', 'release/stable'], | ||
| ['4.4.0', 'release/stable'], | ||
| ['4.0.4-beta1', 'release/beta'], | ||
| ['4.1.0-beta1', 'release/beta'], | ||
| ['4.1.0-beta3', 'release/beta'], | ||
| ])('derives branch %s → %s', (version, expected) => { | ||
| expect(deriveBranch(version)).toBe(expected); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.