-
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
Draft
davseve
wants to merge
35
commits into
main
Choose a base branch
from
ED-24451
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,641
−119
Draft
Changes from all 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 5a9ba70
build: add compiled dist for release-tag-creation action
davseve 293e617
add tests
davseve 1da6c94
build
davseve 3e0375c
add tests infra
davseve b6f19a4
lint
davseve 40d6b85
add tests to version re-write
davseve 6eb0903
wip
davseve b5432e7
summery report
davseve 8a5cb68
wip
davseve 6619d47
Remove v from tag version
davseve 11ce058
fix test
davseve 4f7dc35
change branch model
davseve 351ad86
wip
davseve d8a80b9
check current version is the valid next version
davseve 270cba7
check that the next version is the correct one
davseve b091e15
test coverage for version gap validation
davseve 929fb09
remove ts bundle
davseve 621a0ea
Revert "remove ts bundle"
davseve beac187
remove ts to js bundle
davseve e2965d3
update package lock
davseve 5903c3e
fix: use npm install instead of npm ci for composite action
davseve 63e0778
fix: inline utils functions to make action self-contained
davseve 0112aa5
fix: call run() in main.ts and set ESM module type
davseve c9c8f75
Netanetl's review comments
davseve dedb58f
fix: add .ts extensions to relative imports for Node ESM resolution
davseve 08b73ee
allow extension
davseve 09c77f0
wip
davseve bcdc0fd
wip
davseve 433a5b9
fix: address CodeQL code injection and command injection findings
davseve f4c8adb
remove code duplication
davseve 5daee0f
wip
davseve 39d20b6
lint
davseve 5444eeb
fix: resolve lint failures and activate Elementor in test flow
cursoragent 98007cd
fix: use WordPress 6.8 in performance flow test
cursoragent File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| 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: Install dependencies | ||
| shell: bash | ||
| env: | ||
| ACTION_PATH: ${{ github.action_path }} | ||
| run: npm install --prefix "$ACTION_PATH" | ||
|
|
||
| - name: Handle version input | ||
| id: handle-version-input | ||
| shell: bash | ||
| env: | ||
| INPUT_VERSION: ${{ inputs.version }} | ||
| ACTION_PATH: ${{ github.action_path }} | ||
| run: node "$ACTION_PATH/main.ts" | ||
|
|
||
| - name: Switch to release branch | ||
| shell: bash | ||
| env: | ||
| CHECKOUT_BRANCH: ${{ steps.handle-version-input.outputs.checkout_branch }} | ||
| CHECKOUT_SHA: ${{ github.sha }} | ||
| run: | | ||
| git checkout "${CHECKOUT_BRANCH}" | ||
| git checkout "${CHECKOUT_SHA}" -- .github/ | ||
|
|
||
| - name: Config git user | ||
| shell: bash | ||
| env: | ||
| GIT_USER_NAME: ${{ inputs.maintain_username }} | ||
| GIT_USER_EMAIL: ${{ inputs.maintain_email }} | ||
| run: | | ||
| git config --global user.name "${GIT_USER_NAME}" | ||
| git config --global user.email "${GIT_USER_EMAIL}" | ||
|
|
||
| - name: Update version files | ||
| id: capture-readme | ||
| shell: bash | ||
| env: | ||
| INPUT_VERSION: ${{ inputs.version }} | ||
| INPUT_CHANNEL: ${{ steps.handle-version-input.outputs.channel }} | ||
| INPUT_COMPANION_TAG: ${{ steps.handle-version-input.outputs.companion_tag }} | ||
| ACTION_PATH: ${{ github.action_path }} | ||
| run: node "$ACTION_PATH/update-version-files.ts" | ||
|
|
||
| - name: Commit version bump | ||
| shell: bash | ||
| env: | ||
| VERSION: ${{ inputs.version }} | ||
| DRY_RUN: ${{ inputs.dry_run }} | ||
| run: | | ||
| 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 | ||
| env: | ||
| VERSION: ${{ inputs.version }} | ||
| DRY_RUN: ${{ inputs.dry_run }} | ||
| run: | | ||
| if [[ "${DRY_RUN}" == "true" ]]; then | ||
| echo "Dry run — skipping tag push." | ||
| echo "tag_pushed=false" >> "$GITHUB_OUTPUT" | ||
| else | ||
| git tag -a "${VERSION}" -m "Release version ${VERSION}" | ||
| git push origin "${VERSION}" | ||
| echo "Release tag ${VERSION} pushed successfully (branch unchanged)." | ||
| echo "tag_pushed=true" >> "$GITHUB_OUTPUT" | ||
| fi | ||
|
|
||
| - name: Write job summary | ||
| shell: bash | ||
| env: | ||
| DRY_RUN: ${{ inputs.dry_run }} | ||
| TAG_PUSHED: ${{ steps.create-release-tag.outputs.tag_pushed }} | ||
| CHANNEL: ${{ steps.handle-version-input.outputs.channel }} | ||
| 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 }} | ||
| VERSION: ${{ inputs.version }} | ||
| run: | | ||
| 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** | \`${VERSION}\` |" | ||
| 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" |
100 changes: 100 additions & 0 deletions
100
actions/release-tag-creation/current-version-validation.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| import { execSync } from 'node:child_process'; | ||
| import { beforeEach, describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| import { checkVersionIsNext } from './current-version-validation.ts'; | ||
|
|
||
| vi.mock('node:child_process'); | ||
| vi.mock('node:fs'); | ||
|
|
||
| const mockExecSync = vi.mocked(execSync); | ||
|
|
||
| function makeLsRemoteTags(tags: string[]): string { | ||
| return tags.map((t) => `abc123\trefs/tags/${t}`).join('\n'); | ||
| } | ||
|
|
||
| describe('checkVersionIsNext', () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| }); | ||
|
|
||
| it.each([ | ||
| // stable — patch | ||
| ['stable', ['4.2.0', '4.2.1', '4.2.2'], '4.2.3', true, null], | ||
| [ | ||
| 'stable', | ||
| ['4.2.0', '4.2.1', '4.2.2'], | ||
| '4.2.4', | ||
| false, | ||
| 'Expected next patch release to be 4.2.3', | ||
| ], | ||
| [ | ||
| 'stable', | ||
| ['4.2.0', '4.2.1', '4.2.2'], | ||
| '4.2.2', | ||
| false, | ||
| 'Expected next patch release to be 4.2.3', | ||
| ], | ||
| // stable — minor | ||
| ['stable', ['4.1.0', '4.2.0', '4.2.5'], '4.3.0', true, null], | ||
| [ | ||
| 'stable', | ||
| ['4.1.0', '4.2.0', '4.2.5'], | ||
| '4.4.0', | ||
| false, | ||
| 'Expected next minor release to be 4.3.0', | ||
| ], | ||
| // stable — no existing tags | ||
| ['stable', ['4.1.0-beta1'], '4.1.0', true, null], | ||
| // beta — next in same series | ||
| ['beta', ['4.2.0-beta1', '4.2.0-beta2'], '4.2.0-beta3', true, null], | ||
| [ | ||
| 'beta', | ||
| ['4.2.0-beta1', '4.2.0-beta2'], | ||
| '4.2.0-beta4', | ||
| false, | ||
| 'Expected next beta to be 4.2.0-beta3', | ||
| ], | ||
| [ | ||
| 'beta', | ||
| ['4.2.0-beta1', '4.2.0-beta2'], | ||
| '4.2.0-beta2', | ||
| false, | ||
| 'Expected next beta to be 4.2.0-beta3', | ||
| ], | ||
| // beta — new version line | ||
| ['beta', ['4.2.0-beta1', '4.2.0-beta2'], '4.3.0-beta1', true, null], | ||
| [ | ||
| 'beta', | ||
| ['4.2.0-beta1', '4.2.0-beta2'], | ||
| '4.3.0-beta2', | ||
| false, | ||
| 'must be beta1', | ||
| ], | ||
| // beta — no existing tags | ||
| ['beta', ['4.1.0', '4.2.0'], '4.2.0-beta1', true, null], | ||
| ] as const)( | ||
| '%s %s → %s (valid=%s)', | ||
| (channel, existingTags, version, valid, errorMsg) => { | ||
| mockExecSync.mockReturnValue( | ||
| makeLsRemoteTags([...existingTags]) as never, | ||
| ); | ||
| const fn = () => { | ||
| checkVersionIsNext(version, channel); | ||
| }; | ||
| if (valid) { | ||
| expect(fn).not.toThrow(); | ||
| } else { | ||
| expect(fn).toThrow(errorMsg); | ||
| } | ||
| }, | ||
| ); | ||
|
|
||
| it('throws when git ls-remote fails', () => { | ||
| mockExecSync.mockImplementation(() => { | ||
| throw new Error('network error'); | ||
| }); | ||
| expect(() => { | ||
| checkVersionIsNext('4.2.3', 'stable'); | ||
| }).toThrow('Failed to fetch remote tags'); | ||
| }); | ||
| }); |
104 changes: 104 additions & 0 deletions
104
actions/release-tag-creation/current-version-validation.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| import { execSync } from 'node:child_process'; | ||
| import semver from 'semver'; | ||
|
|
||
| export const ALLOWED_PATTERN = /^\d+\.\d+\.\d+(-beta\d+)?$/; | ||
|
|
||
| function fetchTagsByChannel(channel: 'stable' | 'beta'): string[] { | ||
| let tagsOutput: string; | ||
| try { | ||
| tagsOutput = execSync('git ls-remote --tags origin', { | ||
| encoding: 'utf8', | ||
| }); | ||
| } catch (err) { | ||
| throw new Error( | ||
| `Failed to fetch remote tags: ${(err as Error).message}`, | ||
| ); | ||
| } | ||
|
|
||
| return tagsOutput | ||
| .split('\n') | ||
| .map((line) => line.split('\t')[1]?.replace('refs/tags/', '').trim()) | ||
| .filter((tag): tag is string => { | ||
| if (!tag || !ALLOWED_PATTERN.test(tag)) return false; | ||
|
Comment on lines
+18
to
+22
|
||
| const isBeta = tag.includes('-beta'); | ||
| return channel === 'beta' ? isBeta : !isBeta; | ||
| }); | ||
| } | ||
|
|
||
| function validateNextStable( | ||
| version: string, | ||
| n: semver.SemVer, | ||
| l: semver.SemVer, | ||
| ): void { | ||
| const isNewMinor = n.patch === 0; | ||
| const expected = isNewMinor | ||
| ? `${String(l.major)}.${String(l.minor + 1)}.0` | ||
| : `${String(l.major)}.${String(l.minor)}.${String(l.patch + 1)}`; | ||
| if (version !== expected) { | ||
| const kind = isNewMinor ? 'minor' : 'patch'; | ||
| throw new Error( | ||
| `Expected next ${kind} release to be ${expected}, got ${version} (latest: ${l.version}).`, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| function validateNextBeta( | ||
| version: string, | ||
| n: semver.SemVer, | ||
| l: semver.SemVer, | ||
| latest: string, | ||
| ): void { | ||
| const latestBetaNum = Number(String(l.prerelease[0]).replace('beta', '')); | ||
| const newBetaNum = Number(String(n.prerelease[0]).replace('beta', '')); | ||
| const sameLine = | ||
| n.major === l.major && n.minor === l.minor && n.patch === l.patch; | ||
| if (sameLine && newBetaNum !== latestBetaNum + 1) { | ||
| throw new Error( | ||
| `Expected next beta to be ${latest.replace(`beta${String(latestBetaNum)}`, `beta${String(latestBetaNum + 1)}`)}, got ${version}.`, | ||
| ); | ||
| } | ||
| if (!sameLine && newBetaNum !== 1) { | ||
| throw new Error( | ||
| `First beta of a new version line must be beta1, got ${version}.`, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| export function fetchCompanionTag(channel: 'stable' | 'beta'): string | null { | ||
| const counterpartChannel = channel === 'stable' ? 'beta' : 'stable'; | ||
| const tags = fetchTagsByChannel(counterpartChannel); | ||
| if (tags.length === 0) return null; | ||
| return tags.sort(semver.rcompare)[0] ?? null; | ||
| } | ||
|
|
||
| export function checkVersionIsNext( | ||
| version: string, | ||
| channel: 'stable' | 'beta', | ||
| ): void { | ||
| const channelTags = fetchTagsByChannel(channel); | ||
| if (channelTags.length === 0) { | ||
| console.log( | ||
| `✅ No existing ${channel} tags — treating as first release.`, | ||
| ); | ||
| return; | ||
| } | ||
| const latest = channelTags.sort(semver.rcompare)[0]; | ||
| if (!latest) { | ||
| return; | ||
| } | ||
| const l = semver.parse(latest); | ||
| const n = semver.parse(version); | ||
| if (!l || !n) { | ||
| throw new Error( | ||
| `Failed to parse version strings: latest=${latest}, version=${version}`, | ||
| ); | ||
| } | ||
| if (channel === 'stable') { | ||
| validateNextStable(version, n, l); | ||
| } else { | ||
| validateNextBeta(version, n, l, latest); | ||
| } | ||
| console.log( | ||
| `✅ Version ${version} is the correct next version after ${latest}.`, | ||
| ); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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