diff --git a/.ddev/config.yaml b/.ddev/config.yaml index 55cd30b..fa8dd68 100644 --- a/.ddev/config.yaml +++ b/.ddev/config.yaml @@ -20,6 +20,7 @@ omit_containers: [db, dba] use_dns_when_possible: true composer_version: "2" web_environment: + - COMPOSER_ROOT_VERSION=0.6.0 - XDEBUG_MODE=debug,develop,coverage nodejs_version: "16" diff --git a/.github/actions/dist/release.js b/.github/actions/dist/release.js new file mode 100644 index 0000000..96e1580 --- /dev/null +++ b/.github/actions/dist/release.js @@ -0,0 +1,302 @@ +/** + * Core synchronization action. + * + * @param {@actions/github/GitHub} github + * @param {@actions/github/Context} context + * @param {@actions/core} core + * @param {@actions/exec} exec + * @param {string} version + * @param {string} step + * @returns {void} + */ +module.exports = async ({github, context, core, exec}, version, step) => { + /** + * Log a debug message. + * + * core.debug does not recursively resolve all objects so instead we use the + * console.log which behalfs like expected. + * + * @param {...any} data + * @returns {void} + */ + async function debug( + ...data + ) { + if (!core.isDebug()) { + return + } + + console.log(...data) + } + + /** + * Executes a command and throws in case of a failure. + * + * @param {string} commandLine + * @param {string[]} args + * @param {ExecOptions} options + * @returns {void} + */ + async function safeExec( + commandLine, + args, + options + ) { + const exitCode = await exec.exec(commandLine, args, options) + + if (exitCode > 0) { + throw new Error(`"${commandLine}" terminated with exit code ${exitCode}.`) + } + } + + /** + * Commits all changes with the given message. + * + * @param {string} message + * @returns {void} + */ + async function commitChange( + message + ) { + await safeExec(`git commit -a -m "${message}"`) + } + + /** + * Lookups a pending pull request and returns its number or 0 if + * not found. + * + * @param {string} branch + * @returns {number} + */ + async function getPendingPullRequest( + branch + ) { + const response = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + head: `${context.repo.owner}:${branch}`, + }) + + debug(response) + + if (response.status !== 200) { + throw new Error(`List pull requests failed (${response.status}).`) + } + + var pullRequestNo = 0 + + if (response.data.length > 0) { + pullRequestNo = response.data[0].number + core.notice(`Pending pull request ${pullRequestNo} found.`) + } + + return pullRequestNo + } + + /** + * Returns the default branch of the repository. + * + * @returns {string} + */ + async function getDefaultBranch() { + if (context.payload.repository.default_branch !== undefined) { + return context.payload.repository.default_branch + } + + const response = await github.rest.repos.get({ + owner: context.repo.owner, + repo: context.repo.repo, + }) + + debug(response) + + if (response.status !== 200) { + throw new Error(`Get repository failed (${response.status}).`) + } + + return response.data.default_branch + } + + /** + * Creates a pull request for the given branch and returns its number. + * + * @param {string} branch + * @param {string} title + * @returns {number} + */ + async function createPullRequest( + branch, + title + ) { + const defaultBranch = await getDefaultBranch() + + debug(context.repo.owner) + debug(context.repo.repo) + debug(branch) + debug(version) + debug(step) + debug(defaultBranch) + + const response = await github.rest.pulls.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: title, + head: branch, + base: defaultBranch, + body: ``, + maintainer_can_modify: true, + draft: true, + }) + + debug(response) + + if (response.status !== 201) { + throw new Error(`Create pull request failed (${response.status}).`) + } + + core.notice(`Pull request ${response.data.number} created.`) + + return response.data.number + } + + /** + * Dumps the context if debug mode is enabled. + * + * @returns {void} + */ + async function dumpContext() { + if (!core.isDebug()) { + return + } + + core.startGroup(`Dump context attributes`) + + try { + console.log(context) + } finally { + core.endGroup() + } + } + + /** + * Setups the repository to be able to commit and switches to the + * given branch. + * + * @param {string} branch + * @returns {void} + */ + async function setupRepository( + branch + ) { + core.startGroup(`Setup repository`) + + try { + await safeExec(`git config user.name github-actions`) + await safeExec(`git config user.email github-actions@github.com`) + await safeExec(`git branch ${branch}`) + await safeExec(`git switch ${branch}`) + } finally { + core.endGroup() + } + } + + /** + * Create release or version commit. + * + * @param {string} version + * @param {string} step + * @returns {string} + */ + async function createCommit( + version, + step + ) { + core.startGroup('Create release commit') + + try { + await safeExec(`composer set-version ${version}`) + + let commitMessage = '' + if (step === 'version') { + commitMessage = `[TASK] Set TYPO3 Coding Standards version to ${version}` + } else { + commitMessage = `[RELEASE] Release of TYPO3 Coding Standards ${version}` + } + + await commitChange(commitMessage) + + return commitMessage + } finally { + core.endGroup() + } + } + + /** + * Push changes to the provided branch. + * + * @param {string} branch + * @returns {void} + */ + async function pushChanges( + branch + ) { + core.startGroup(`Push changes`) + + try { + await safeExec(`git push -f origin ${branch}`) + } finally { + core.endGroup() + } + } + + /** + * Create a pull request or update an existing one. + * + * @param {string} branch + * @param {string} title + * @returns {number} + */ + async function createOrUpdatePullRequest( + branch, + title + ) { + core.startGroup(`Create pull request`) + + try { + let pullRequestNo = await getPendingPullRequest(branch) + + if (pullRequestNo !== 0) { + return pullRequestNo + } + + pullRequestNo = await createPullRequest(branch, title) + + return pullRequestNo + } finally { + core.endGroup() + } + } + + try { + dumpContext() + + let pullRequestBranch = '' + if (step === 'version') { + pullRequestBranch = 'release/version' + } else { + pullRequestBranch = 'release/release' + } + + await setupRepository(pullRequestBranch) + + const commitMessage = await createCommit(version, step) + + let pullRequestNo = 0 + await pushChanges(pullRequestBranch) + pullRequestNo = await createOrUpdatePullRequest(pullRequestBranch, commitMessage) + + core.setOutput('pull-request', pullRequestNo) + } catch (err) { + core.setFailed(`Action failed with error ${err}`) + } +} diff --git a/.github/release-drafter-dev.yml b/.github/release-drafter-dev.yml new file mode 100644 index 0000000..be4a4a8 --- /dev/null +++ b/.github/release-drafter-dev.yml @@ -0,0 +1,8 @@ +name-template: 'TYPO3 Coding Standards Package $RESOLVED_VERSION' +tag-template: '$RESOLVED_VERSION' +version-template: '$MAJOR.$MINOR.$PATCH-dev' +prerelease: true +version-resolver: + default: minor +filter-by-commitish: true +template: _⚠⚠⚠ Draft to calculate next release. Will be overwritten with the next release and can be deleted at any time ⚠⚠⚠_ diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..cbbad3a --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,43 @@ +template: | + ## What's Changed Since $PREVIOUS_TAG + + $CHANGES + + **Full Changelog**: +category-template: '### $TITLE' +name-template: 'TYPO3 Coding Standards Package $RESOLVED_VERSION' +tag-template: 'v$RESOLVED_VERSION' +tag-prefix: 'v' +version-template: '$COMPLETE' +change-template: '- $TITLE by @$AUTHOR in <$URL>' +change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. +categories: + - title: '🚀 Features' + label: 'enhancement' + - title: '🐞 Bug Fixes' + label: 'bug' + - title: '🧰 Maintenance' + label: 'maintenance' + - title: '📖 Documentation' + label: 'documentation' + - title: '🔓 Security' + label: 'security' + - title: '⚠ Breaking' + label: 'breaking' +exclude-labels: + - 'skip-changelog' +version-resolver: + major: + labels: + - 'breaking' + minor: + labels: + - 'enhancement' + patch: + labels: + - 'bug' + - 'documentation' + - 'maintenance' + - 'security' + default: patch +filter-by-commitish: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..eca20fb --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,219 @@ +name: Create Release + +on: + workflow_dispatch: + inputs: + stability: + description: 'Stability' + required: true + default: 'RC' + type: choice + options: + - dev + - alpha + - beta + - RC + - stable + stabilityVersion: + description: 'Stability version, not used for stable' + required: false + default: '' + type: string + releaseDescription: + description: 'Optional release description' + required: false + default: '' + type: string + +env: + COMPOSER_FLAGS: --ansi --no-interaction --no-progress + COMPOSER_INSTALL_FLAGS: --prefer-dist + +jobs: + create_release: + name: Create release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + coverage: none + extensions: intl, zip + ini-values: memory_limit=-1 + php-version: latest + tools: composer + + - name: Composer Cache Vars + id: composer-cache-vars + run: | + echo "::set-output name=dir::$(composer config cache-files-dir)" + echo "::set-output name=timestamp::$(date +"%s")" + + - name: Cache Composer dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache-vars.outputs.dir }} + key: ${{ runner.os }}-composer-2-latest-${{ steps.composer-cache-vars.outputs.timestamp }} + restore-keys: | + ${{ runner.os }}-composer-2-latest- + ${{ runner.os }}-composer-2- + ${{ runner.os }}-composer- + + - name: Install dependencies + run: composer install ${{ env.COMPOSER_INSTALL_FLAGS }} ${{ env.COMPOSER_FLAGS }} + + - name: Calculate header + id: header + uses: actions/github-script@v6 + env: + INPUTS: ${{ toJSON(inputs) }} + with: + result-encoding: string + script: | + const { INPUTS } = process.env + const inputs = JSON.parse(INPUTS) + + let header = '' + + if (inputs.releaseDescription != undefined && inputs.releaseDescription != '') { + header = `${inputs.releaseDescription}\n\n` + } + + return header + + - name: Create release draft + id: tempVersion + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: release-drafter/release-drafter@v5 + with: + prerelease: ${{ inputs.stability != 'stable' }} + commitish: ${{ github.ref }} + disable-autolabeler: true + + - name: Calculate final version + id: version + uses: actions/github-script@v6 + env: + INPUTS: ${{ toJSON(inputs) }} + VERSION_OUTPUTS: ${{ toJSON(steps.tempVersion.outputs) }} + with: + script: | + const { INPUTS, VERSION_OUTPUTS } = process.env + const inputs = JSON.parse(INPUTS) + const versionOutputs = JSON.parse(VERSION_OUTPUTS) + + const version = String(versionOutputs.tag_name).substring(1) + let versionSuffix = '' + + if (inputs.stability != 'stable') { + versionSuffix = versionSuffix.concat('-', inputs.stability) + + if (inputs.stabilityVersion == undefined || inputs.stabilityVersion == '') { + core.setFailed('For a stability other than stable, a stability version is needed.') + return + } + + versionSuffix = versionSuffix.concat(inputs.stabilityVersion) + } + + const completeVersion = version.concat(versionSuffix) + const name = String(versionOutputs.name).concat(versionSuffix) + + const options = {}; + options.ignoreReturnCode = true + const exitCode = await exec.exec(`git show-ref --tags "refs/tags/v${completeVersion}" >/dev/null 2>&1`, [], options) + + if (exitCode == 0) { + core.setFailed(`A tag v${completeVersion} does already exist.`) + } + + core.setOutput('version', completeVersion) + core.setOutput('prerelease', inputs.stability != 'stable') + core.setOutput('name', name) + core.setOutput('tag_name', String('v').concat(completeVersion)) + core.setOutput('body', versionOutputs.body) + + - name: Print calculated version + env: + VERSION_OUTPUTS: ${{ toJSON(steps.version.outputs) }} + run: | + echo "$VERSION_OUTPUTS" + + - name: Modify changelog + uses: actions/github-script@v6 + env: + VERSION_OUTPUTS: ${{ toJSON(steps.version.outputs) }} + REPOSITORY: ${{ toJSON(github.event.repository) }} + with: + script: | + const { VERSION_OUTPUTS, REPOSITORY } = process.env + const version = JSON.parse(VERSION_OUTPUTS) + + const tagName = version.tag_name + const body = String(version.body).replaceAll('# ', '## ').trim() + const repository = REPOSITORY.full_name + + const today = new Date() + const date = String(today.getFullYear()).concat('-', String(today.getMonth() + 1).padStart(2, '0'), '-', String(today.getDate()).padStart(2, '0')); + const title = `## [${tagName}](https://github.com/${repository}/releases/tag/${tagName}) - ${date}` + + const changelogPath = 'CHANGELOG.md' + const fs = require('fs') + const changelog = fs.readFileSync(changelogPath, 'utf8') + const marker = '(start of releases)' + const newChangelog = changelog.replace(marker, marker.concat('\n\n', title, '\n\n', body)) + fs.writeFileSync(changelogPath, newChangelog) + + - name: Add release commit + env: + SENDER: ${{ toJSON(github.event.sender) }} + run: | + composer set-version ${{ steps.version.outputs.version }} + git config user.name ${{ github.event.sender.login }} + git config user.email ${{ github.event.sender.id }}+${{ github.event.sender.login }}@users.noreply.github.com + git add . + git commit -m "[RELEASE] Release of ${{ steps.version.outputs.name }}" + git push + + - name: Create release + id: release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: release-drafter/release-drafter@v5 + with: + #name: ${{ steps.version.outputs.name }} + #tag: ${{ steps.version.outputs.tag_name }} + version: ${{ steps.version.outputs.version }} + publish: true + prerelease: ${{ steps.version.outputs.prerelease }} + commitish: ${{ github.ref }} + header: ${{ steps.header.result }} + disable-autolabeler: true + + - name: Calculate next version + id: next + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: release-drafter/release-drafter@v5 + with: + config-name: release-drafter-dev.yml + prerelease: true + commitish: ${{ github.ref }} + disable-autolabeler: true + + - name: Print calculated version + env: + VERSION_OUTPUTS: ${{ toJSON(steps.next.outputs) }} + run: | + echo "$VERSION_OUTPUTS" + + - name: Add version commit + run: | + composer set-version ${{ steps.next.outputs.tag_name }} + git add . + git commit -m "[TASK] Set TYPO3 Coding Standards version to ${{ steps.next.outputs.tag_name }}" + git push diff --git a/build/ReleaseScripts.php b/build/ReleaseScripts.php new file mode 100644 index 0000000..271180d --- /dev/null +++ b/build/ReleaseScripts.php @@ -0,0 +1,56 @@ +