diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 049e00d396..bf4e8ee2af 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -84,8 +84,8 @@ jobs: run: gh api -X DELETE repos/${{ github.repository }}/git/refs/heads/release-candidate || echo "Already deleted" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Remove release branch - run: gh api -X DELETE repos/${{ github.repository }}/git/refs/heads/release + - name: Remove release-beta branch + run: gh api -X DELETE repos/${{ github.repository }}/git/refs/heads/release-beta env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Disable Release workflow diff --git a/private/release/autoFixConflicts.js b/private/release/autoFixConflicts.js new file mode 100755 index 0000000000..0cb29a580f --- /dev/null +++ b/private/release/autoFixConflicts.js @@ -0,0 +1,56 @@ +#!/usr/bin/env node + +// Usage: autoFixConflicts.js | sh + +import { createInterface as readLines } from 'node:readline' +import { spawn } from 'node:child_process' + +const VERSION_URL = /(?<=https:\/\/\S+\/v)\d+\.\d+\.\d+(?:-(?:alpha|beta)(?:[.-]\d+)?)?(?=\/)/ + +const gitStatus = spawn('git', ['status', '--porcelain']) + +for await (const line of readLines(gitStatus.stdout)) { + // eslint-disable-next-line no-continue + if (!line.startsWith('UU ')) continue + + const file = line.slice(3) + if (file === 'yarn.lock') { + console.log('corepack yarn install') + console.log('git add yarn.lock') + // eslint-disable-next-line no-continue + continue + } + + if (file.endsWith('/package.json')) { + console.log(`git checkout --ours ${file}`) + console.log(`git add ${file}`) + // eslint-disable-next-line no-continue + continue + } + + const gitDiff = spawn('git', ['--no-pager', 'diff', '--', file]) + let conflictHasStarted = false + let containsCDNChanges = true + let currentConflictContainsCDNChanges = false + + // eslint-disable-next-line no-shadow + for await (const line of readLines(gitDiff.stdout)) { + if (conflictHasStarted) { + if (line.startsWith('++>>>>>>>')) { + conflictHasStarted = false + containsCDNChanges &&= currentConflictContainsCDNChanges + currentConflictContainsCDNChanges = false + } else { + currentConflictContainsCDNChanges ||= VERSION_URL.test(line) + } + } else if (line === '++<<<<<<< HEAD') { + conflictHasStarted = true + } + } + if (containsCDNChanges) { + console.log(`git checkout --ours ${file}`) + console.log(`git add ${file}`) + // eslint-disable-next-line no-continue + continue + } +} diff --git a/private/release/choose-semverness.js b/private/release/choose-semverness.js index 3a7654c1f4..965d20526a 100755 --- a/private/release/choose-semverness.js +++ b/private/release/choose-semverness.js @@ -26,6 +26,7 @@ function maxSemverness (a, b) { export default async function pickSemverness ( spawnOptions, LAST_RELEASE_COMMIT, + STABLE_BRANCH_MERGE_BASE_RANGE, releaseFileUrl, packagesList, ) { @@ -55,7 +56,28 @@ export default async function pickSemverness ( spawnOptions, ) if (stdout.length === 0) { - console.log(`No commits since last release for ${name}, skipping.`) + const { stdout } = spawnSync( + 'git', + [ + '--no-pager', + 'log', + '--format=- %s', + STABLE_BRANCH_MERGE_BASE_RANGE, + '--', + location, + ], + spawnOptions, + ) + if (stdout.length === 0) { + console.log(`No commits since last release for ${name}, skipping.`) + } else { + console.log(`Some commits have landed on the stable branch since last release for ${name}.`) + releaseFile.write(` ${JSON.stringify(name)}: prerelease\n`) + uppySemverness = maxSemverness(uppySemverness, 'prerelease') + if (robodogDeps.includes(name)) { + robodogSemverness = maxSemverness(robodogSemverness, 'prerelease') + } + } continue } console.log('\n') diff --git a/private/release/commit-and-open-pr.js b/private/release/commit-and-open-pr.js index 7cf9de1414..d7325c9e06 100644 --- a/private/release/commit-and-open-pr.js +++ b/private/release/commit-and-open-pr.js @@ -1,9 +1,9 @@ import { spawnSync } from 'node:child_process' import { fileURLToPath } from 'node:url' import prompts from 'prompts' -import { REPO_OWNER, REPO_NAME } from './config.js' +import { REPO_NAME, REPO_OWNER } from './config.js' -export default async function commit (spawnOptions, ...files) { +export default async function commit (spawnOptions, STABLE_HEAD, ...files) { console.log(`Now is the time to do manual edits to ${files.join(',')}.`) await prompts({ type: 'toggle', @@ -15,8 +15,59 @@ export default async function commit (spawnOptions, ...files) { }) spawnSync('git', ['add', ...files.map(url => fileURLToPath(url))], spawnOptions) + const remoteHeadSha = spawnSync('git', ['rev-parse', 'HEAD'], spawnOptions).stdout.toString().trim() spawnSync('git', ['commit', '-n', '-m', 'Prepare next release'], { ...spawnOptions, stdio: 'inherit' }) + const releaseSha = spawnSync('git', ['rev-parse', 'HEAD'], spawnOptions).stdout.toString().trim() + + console.log('Attempting to merge changes from stable branch...') + { + // eslint-disable-next-line no-shadow + const { status, stdout, stderr } = spawnSync( + 'git', + [ + 'merge', + '--no-edit', + '-m', + 'Merge stable branch', + STABLE_HEAD, + ], + spawnOptions, + ) + if (status) { + console.log(stdout.toString()) + console.error(stderr.toString()) + + await prompts({ + type: 'toggle', + name: 'value', + message: 'Fix the conflict before continuing. Ready?', + initial: true, + active: 'yes', + inactive: 'yes', + }) + + // eslint-disable-next-line no-shadow + const { status } = spawnSync( + 'git', + [ + 'merge', + '--continue', + ], + { ...spawnOptions, stdio: 'inherit' }, + ) + + if (status) { + throw new Error('Merge has failed') + } + } + } + const mergeSha = spawnSync('git', ['rev-parse', 'HEAD'], spawnOptions).stdout.toString().trim() + + spawnSync('git', ['reset', remoteHeadSha, '--hard'], spawnOptions) + spawnSync('git', ['cherry-pick', mergeSha, '--hard'], spawnOptions) + spawnSync('git', ['cherry-pick', releaseSha, '--hard'], spawnOptions) const sha = spawnSync('git', ['rev-parse', 'HEAD'], spawnOptions).stdout.toString().trim() + const getRemoteCommamnd = `git remote -v | grep '${REPO_OWNER}/${REPO_NAME}' | awk '($3 == "(push)") { print $1; exit }'` const remote = spawnSync('/bin/sh', ['-c', getRemoteCommamnd]).stdout.toString().trim() || `git@github.com:${REPO_OWNER}/${REPO_NAME}.git` diff --git a/private/release/config.js b/private/release/config.js index b6a6c7ec50..21d9798166 100644 --- a/private/release/config.js +++ b/private/release/config.js @@ -1,3 +1,4 @@ export const REPO_OWNER = 'transloadit' export const REPO_NAME = 'uppy' export const TARGET_BRANCH = '3.x' +export const STABLE_BRANCH = 'main' diff --git a/private/release/getUpToDateRefsFromGitHub.js b/private/release/getUpToDateRefsFromGitHub.js index e67d3cb130..7989568649 100644 --- a/private/release/getUpToDateRefsFromGitHub.js +++ b/private/release/getUpToDateRefsFromGitHub.js @@ -2,7 +2,7 @@ import fetch from 'node-fetch' import { spawnSync } from 'node:child_process' import prompts from 'prompts' -import { TARGET_BRANCH, REPO_NAME, REPO_OWNER } from './config.js' +import { TARGET_BRANCH, REPO_NAME, REPO_OWNER, STABLE_BRANCH } from './config.js' async function apiCall (endpoint, errorMessage) { const response = await fetch( @@ -25,10 +25,10 @@ export async function getRemoteHEAD () { } async function getLatestReleaseSHA () { - const { tag_name } = await apiCall( - `/releases/latest`, - 'Cannot get latest release from GitHub, check your internet connection.', - ) + const response = await fetch(`https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/${TARGET_BRANCH}/packages/uppy/package.json`) + if (!response.ok) throw new Error(`Network call failed: ${response.status} ${response.statusText}`) + const { version } = await response.json() + const tag_name = `uppy@${version}` console.log(`Last release was ${tag_name}.`) return ( await apiCall( @@ -38,6 +38,15 @@ async function getLatestReleaseSHA () { ).object.sha } +function getStableBranchMergeBase (REMOTE_HEAD) { + spawnSync('git', ['fetch', `https://github.com/${REPO_OWNER}/${REPO_NAME}.git`, STABLE_BRANCH]) + const STABLE_HEAD = spawnSync('git', ['rev-parse', 'FETCH_HEAD']).stdout.toString().trim() + return [[ + spawnSync('git', ['merge-base', REMOTE_HEAD, 'FETCH_HEAD']).stdout.toString().trim(), + STABLE_HEAD, + ].join('..'), STABLE_HEAD] +} + async function getLocalHEAD () { return spawnSync('git', ['rev-parse', 'HEAD']).stdout.toString().trim() } @@ -104,5 +113,5 @@ export async function validateGitStatus (spawnOptions) { } } - return [await latestRelease, LOCAL_HEAD] + return [await latestRelease, LOCAL_HEAD, ...getStableBranchMergeBase(REMOTE_HEAD)] } diff --git a/private/release/interactive.js b/private/release/interactive.js index c62af916c0..6b1c8be272 100755 --- a/private/release/interactive.js +++ b/private/release/interactive.js @@ -14,14 +14,14 @@ const deferredReleaseFile = new URL('./.yarn/versions/next.yml', ROOT) const temporaryChangeLog = new URL('./CHANGELOG.next.md', ROOT) console.log('Validating local repo status and get previous release info...') -const [LAST_RELEASE_COMMIT, LOCAL_HEAD] = await validateGitStatus(spawnOptions) +const [LAST_RELEASE_COMMIT, LOCAL_HEAD, MERGE_BASE, STABLE_HEAD] = await validateGitStatus(spawnOptions) try { console.log('Local git repository is ready, starting release process...') - await pickSemverness(spawnOptions, LAST_RELEASE_COMMIT, deferredReleaseFile, process.env.PACKAGES.split(' ')) + await pickSemverness(spawnOptions, LAST_RELEASE_COMMIT, MERGE_BASE, deferredReleaseFile, process.env.PACKAGES.split(' ')) console.log('Working on the changelog...') await formatChangeLog(spawnOptions, LAST_RELEASE_COMMIT, temporaryChangeLog) console.log('Final step...') - await commit(spawnOptions, deferredReleaseFile, temporaryChangeLog) + await commit(spawnOptions, STABLE_HEAD, deferredReleaseFile, temporaryChangeLog) } finally { console.log('Rewinding git history...') await rewindGitHistory(spawnOptions, LOCAL_HEAD)