diff --git a/.github/workflows/release-connect-bump-versions.yml b/.github/workflows/release-connect-bump-versions.yml new file mode 100644 index 000000000000..28fe084487b9 --- /dev/null +++ b/.github/workflows/release-connect-bump-versions.yml @@ -0,0 +1,45 @@ +name: "[Release] connect bump versions" + +on: + workflow_dispatch: + inputs: + semver: + type: choice + description: semver + options: + - patch + - minor + - prerelease + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + bump-versions: + if: github.repository == 'trezor/trezor-suite' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # Number of commits to fetch. 0 indicates all history for all branches and tags. + fetch-depth: 0 + submodules: true + + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + + - name: Install dependencies + run: yarn install + + - name: Set git for trezor-ci + run: | + git config --global user.name "trezor-ci" + git config --global user.email "${{ secrets.TREZOR_BOT_EMAIL }}" + + - name: Check dependencies to update + run: | + node ./ci/scripts/connect-bump-versions.js ${{ github.event.inputs.semver }} diff --git a/.github/workflows/release-connect-init.yml b/.github/workflows/release-connect-init.yml new file mode 100644 index 000000000000..687089424b2e --- /dev/null +++ b/.github/workflows/release-connect-init.yml @@ -0,0 +1,83 @@ +name: "[Release] connect NPM and v9" + +permissions: + id-token: write # for fetching the OIDC token + contents: read # for actions/checkout + +on: + workflow_dispatch: + inputs: + commit_sha: + description: "The commit SHA to checkout" + required: true + type: string + +jobs: + # Version should have been bumped by now thanks to ./ci/scripts/connect-release-init-npm.js + extract-version: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.set-version.outputs.version }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # Number of commits to fetch. 0 indicates all history for all branches and tags. + fetch-depth: 0 + # Checkout the specified commit + ref: ${{ github.event.inputs.commit_sha }} + + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + + - name: Extract connect version + id: set-version + run: echo "version=$(node ./ci/scripts/get-connect-version.js)" >> $GITHUB_OUTPUT + + create-push-release-branch: + needs: [extract-version] + name: "Create release branch for version ${{ needs.extract-version.outputs.version }}" + runs-on: ubuntu-latest + outputs: + branch_name: ${{ steps.push-branch.outputs.branch_name }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.TREZOR_BOT_TOKEN }} + fetch-depth: 0 + # Checkout the specified commit + ref: ${{ github.event.inputs.commit_sha }} + + - name: Setup Git config + run: | + git config --global user.name "trezor-ci" + git config --global user.email "${{ secrets.TREZOR_BOT_EMAIL }}" + + - name: Create and push new branch + env: + GITHUB_TOKEN: ${{ secrets.TREZOR_BOT_TOKEN }} + BRANCH_NAME: "release/connect/${{ needs.extract-version.outputs.version }}" + run: | + echo ${{ env.BRANCH_NAME }} + git checkout -b ${{ env.BRANCH_NAME }} + git push origin ${{ env.BRANCH_NAME }} + echo "branch_name=${{ env.BRANCH_NAME }}" >> $GITHUB_OUTPUT + + trigger-staging-release: + runs-on: ubuntu-latest + steps: + - run: gh workflow run .github/workflows/release-connect-v9-staging.yml --ref $BRANCH_NAME + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH_NAME: ${{ needs.create-push-release-branch.outputs.branch_name }} + + trigger-npm-release: + runs-on: ubuntu-latest + steps: + - run: gh workflow run .github/workflows/release-connect-npm-init.yml --ref $BRANCH_NAME + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH_NAME: ${{ needs.create-push-release-branch.outputs.branch_name }} diff --git a/.github/workflows/release-connect-npm-init.yml b/.github/workflows/release-connect-npm-init.yml index ff644f2865ae..37ccc2046f61 100644 --- a/.github/workflows/release-connect-npm-init.yml +++ b/.github/workflows/release-connect-npm-init.yml @@ -16,7 +16,36 @@ concurrency: cancel-in-progress: true jobs: - pre-release: + extract-version-from-package-json: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.set-version.outputs.version }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # Number of commits to fetch. 0 indicates all history for all branches and tags. + fetch-depth: 0 + + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + + - name: Extract connect version + id: set-version + run: echo "version=$(node ./ci/scripts/get-connect-version.js)" >> $GITHUB_OUTPUT + + check-version-match: + runs-on: ubuntu-latest + needs: [extract-version] + uses: ./.github/workflows/template-check-version-match.yml + with: + branch_ref: "${{ github.ref }}" + extracted_version: "${{ needs.extract-version.outputs.version }}" + + trigger-npm-release: + needs: [check-version-match] if: github.repository == 'trezor/trezor-suite' runs-on: ubuntu-latest steps: @@ -43,8 +72,6 @@ jobs: run: | git config --global user.name "trezor-ci" git config --global user.email "${{ secrets.TREZOR_BOT_EMAIL }}" - gh config set prompt disabled - gh api /user --jq .login - node ./ci/scripts/connect-release-init-npm.js ${{ github.event.inputs.semver }} + node ./ci/scripts/connect-release-npm-init.js ${{ github.event.inputs.semver }} release/connect/${{ needs.extract-version.outputs.version }} env: GITHUB_TOKEN: ${{ secrets.TREZOR_BOT_TOKEN }} diff --git a/.github/workflows/release-connect-v9-poduction.yml b/.github/workflows/release-connect-v9-poduction.yml index c22632269b83..cb670ad09db1 100644 --- a/.github/workflows/release-connect-v9-poduction.yml +++ b/.github/workflows/release-connect-v9-poduction.yml @@ -36,6 +36,14 @@ jobs: id: set-version run: echo "version=$(node ./ci/scripts/get-connect-version.js)" >> $GITHUB_OUTPUT + check-version-match: + runs-on: ubuntu-latest + needs: [extract-version] + uses: ./.github/workflows/template-check-version-match.yml + with: + branch_ref: "${{ github.ref }}" + extracted_version: "${{ needs.extract-version.outputs.version }}" + # set the rollback sync-rollback-connect-v9: if: startsWith(github.ref, 'refs/heads/release/connect/') diff --git a/.github/workflows/release-connect-v9-staging.yml b/.github/workflows/release-connect-v9-staging.yml index 0687f3964d84..a54d8b6a392b 100644 --- a/.github/workflows/release-connect-v9-staging.yml +++ b/.github/workflows/release-connect-v9-staging.yml @@ -32,20 +32,10 @@ jobs: check-version-match: runs-on: ubuntu-latest needs: [extract-version] - steps: - - name: Check if version in package.json matches the one in branch name - run: | - # Extract the version from the branch name, assuming format 'refs/heads/release/connect/9.2.4-beta.1' - BRANCH_VERSION="${GITHUB_REF#*release/connect/}" # This strips everything before and including 'release/connect/' - EXTRACTED_VERSION="${{ needs.extract-version.outputs.version }}" - echo "Branch Version: $BRANCH_VERSION" - echo "Extracted Version: $EXTRACTED_VERSION" - if [[ "$BRANCH_VERSION" != "$EXTRACTED_VERSION" ]]; then - echo "The extracted version ($EXTRACTED_VERSION) does not match the version in the branch name ($BRANCH_VERSION)" - exit 1 # Fail the job if versions don't match - else - echo "Version check passed: $BRANCH_VERSION matches $EXTRACTED_VERSION" - fi + uses: ./.github/workflows/template-check-version-match.yml + with: + branch_ref: "${{ github.ref }}" + extracted_version: "${{ needs.extract-version.outputs.version }}" # This job deploys to staging-connect.trezor.io/9.x.x deploy-staging-semantic-version: diff --git a/.github/workflows/template-check-version-match.yml b/.github/workflows/template-check-version-match.yml new file mode 100644 index 000000000000..309bfcfa61a8 --- /dev/null +++ b/.github/workflows/template-check-version-match.yml @@ -0,0 +1,37 @@ +name: Check Version Match + +on: + workflow_call: + inputs: + branch_ref: + description: "The full ref of the branch" + required: true + type: string + extracted_version: + description: "The version extracted from the package.json or other source" + required: true + type: string + +jobs: + check-version-match: + runs-on: ubuntu-latest + steps: + - name: Extract branch version + id: extract-branch-version + run: | + BRANCH_REF="${{ inputs.branch_ref }}" + BRANCH_VERSION="${BRANCH_REF#refs/heads/release/connect/}" + echo "branch_version=$BRANCH_VERSION" >> $GITHUB_OUTPUT + + - name: Check if version in package.json matches the one in branch name + run: | + BRANCH_VERSION="${{ steps.extract-branch-version.outputs.branch_version }}" + EXTRACTED_VERSION="${{ inputs.extracted_version }}" + echo "Branch Version: $BRANCH_VERSION" + echo "Extracted Version: $EXTRACTED_VERSION" + if [[ "$BRANCH_VERSION" != "$EXTRACTED_VERSION" ]]; then + echo "The extracted version ($EXTRACTED_VERSION) does not match the version in the branch name ($BRANCH_VERSION)" + exit 1 # Fail the job if versions don't match + else + echo "Version check passed: $BRANCH_VERSION matches $EXTRACTED_VERSION" + fi diff --git a/ci/scripts/check-npm-and-local.js b/ci/scripts/check-npm-and-local.js index 19b88c6bf8bf..7ef7ee0fdaa3 100644 --- a/ci/scripts/check-npm-and-local.js +++ b/ci/scripts/check-npm-and-local.js @@ -2,7 +2,6 @@ const { execSync } = require('child_process'); const fs = require('fs'); const util = require('util'); -const https = require('https'); const fetch = require('cross-fetch'); const tar = require('tar'); const path = require('path'); diff --git a/ci/scripts/connect-release-init-npm.js b/ci/scripts/connect-bump-versions.js similarity index 75% rename from ci/scripts/connect-release-init-npm.js rename to ci/scripts/connect-bump-versions.js index be11ba64964d..5a0c2b0672af 100644 --- a/ci/scripts/connect-release-init-npm.js +++ b/ci/scripts/connect-bump-versions.js @@ -28,35 +28,12 @@ const getGitCommitByPackageName = (packageName, maxCount = 10) => `./packages/${packageName}`, ]); -const ghWorkflowRunReleaseAction = (branch, packages, deployment) => - exec('gh', [ - 'workflow', - 'run', - '.github/workflows/release-connect-npm.yml', - '--ref', - branch, - '--field', - `packages=${packages}`, - '--field', - `deploymentType=${deployment}`, - ]); - const splitByNewlines = input => input.split('\n'); const findIndexByCommit = (commitArr, searchString) => commitArr.findIndex(commit => commit.includes(searchString)); -const initConnectRelease = async () => { - console.log('Using GitHub Token:', process.env.GITHUB_TOKEN ? 'Yes' : 'No'); - - if (process.env.GITHUB_TOKEN) { - // Making sure we use the proper GITHUB_TOKEN - exec('gh', ['auth', 'setup-git']); - exec('gh', ['config', 'set', '-h', 'github.com', 'oauth_token', process.env.GITHUB_TOKEN]); - } else { - throw new Error('Missing GITHUB_TOKEN'); - } - +const bumpConnect = async () => { const checkResult = await checkPackageDependencies('connect', deploymentType); const update = checkResult.update.map(package => package.replace('@trezor/', '')); @@ -121,7 +98,7 @@ const initConnectRelease = async () => { const { version } = packageJSON; const commitMessage = `npm-release: @trezor/connect ${version}`; - const branchName = `npm-release/connect-${version}`; + const branchName = `bump-versions/connect-${version}`; // Check if branch exists and if so, delete it. const branchExists = exec('git', ['branch', '--list', branchName]).stdout; @@ -207,34 +184,6 @@ const initConnectRelease = async () => { body: connectGitLogText, }); } - - // At this point we have created the commit with the bumped versions, - // and a pull request including all the changes. - // Now we want to trigger the action that will trigger the actual release, - // after approval form authorized member. - const dependenciesToRelease = JSON.stringify(update); - console.log('dependenciesToRelease:', dependenciesToRelease); - console.log('deploymentType:', deploymentType); - console.log('branchName:', branchName); - - const releaseDependencyActionOutput = ghWorkflowRunReleaseAction( - branchName, - dependenciesToRelease, - deploymentType, - ); - - console.log('releaseDependencyActionOutput output:', releaseDependencyActionOutput.stdout); - - // We trigger this second action to release connect, so we can just not approve it in case - // the release of the dependencies to NPM was not successful. - console.log('Triggering action to release connect.'); - const releaseConnectActionOutput = ghWorkflowRunReleaseAction( - branchName, - JSON.stringify(['connect', 'connect-web', 'connect-webextension']), - deploymentType, - ); - - console.log('releaseConnectActionOutput output:', releaseConnectActionOutput.stdout); }; -initConnectRelease(); +bumpConnect(); diff --git a/ci/scripts/connect-release-init-v9.js b/ci/scripts/connect-release-init-v9.js deleted file mode 100644 index 19a39bea750c..000000000000 --- a/ci/scripts/connect-release-init-v9.js +++ /dev/null @@ -1,28 +0,0 @@ -/* eslint-disable camelcase */ - -// TODO: this is still used by the GitLab workflow -// TODO: let's leave it here until we are confident GitHub release works. - -const path = require('path'); -const fs = require('fs'); - -const { exec } = require('./helpers'); - -const ROOT = path.join(__dirname, '..', '..'); - -const init = () => { - const PACKAGE_PATH = path.join(ROOT, 'packages', 'connect'); - const PACKAGE_JSON_PATH = path.join(PACKAGE_PATH, 'package.json'); - const rawPackageJSON = fs.readFileSync(PACKAGE_JSON_PATH); - const packageJSON = JSON.parse(rawPackageJSON); - const { version } = packageJSON; - - // Version should have been bumped by now thanks to ./ci/scripts/connect-release-init-npm.js - const branchName = `release/connect/${version}`; - - exec('git', ['checkout', '-b', branchName]); - - exec('git', ['push', 'origin', branchName]); -}; - -init(); diff --git a/ci/scripts/connect-release-npm-init.js b/ci/scripts/connect-release-npm-init.js new file mode 100644 index 000000000000..149bfbaac951 --- /dev/null +++ b/ci/scripts/connect-release-npm-init.js @@ -0,0 +1,135 @@ +// This script should check what packages are from the repository have different most recent version in NPM +// as the on e in the package.json and trigger the workflow to release to NPM those packages. + +const { exec } = require('./helpers'); + +const fs = require('fs'); +const util = require('util'); +const path = require('path'); +const fetch = require('cross-fetch'); +const semver = require('semver'); + +const args = process.argv.slice(2); + +if (args.length < 2) + throw new Error('Check npm dependencies requires 2 parameters: semver branchName'); +const [semver, branchName] = args; + +const allowedSemvers = ['patch', 'minor', 'prerelease']; +if (!allowedSemvers.includes(semver)) { + throw new Error(`provided semver: ${semver} must be one of ${allowedSemvers.join(', ')}`); +} + +const deploymentType = semver === 'prerelease' ? 'canary' : 'stable'; + +const readFile = util.promisify(fs.readFile); + +const ROOT = path.join(__dirname, '..', '..'); + +const triggerReleaseNpmWorkflow = (branch, packages, deployment) => + exec('gh', [ + 'workflow', + 'run', + '.github/workflows/release-connect-npm.yml', + '--ref', + branch, + '--field', + `packages=${packages}`, + '--field', + `deploymentType=${deployment}`, + ]); + +const getNpmRemoteGreatestVersion = async moduleName => { + const [_prefix] = moduleName.split('/'); + const npmRegistryUrl = `https://registry.npmjs.org/${moduleName}`; + + try { + console.log(`fetching npm registry info from: ${npmRegistryUrl}`); + const response = await fetch(npmRegistryUrl); + const data = await response.json(); + if (data.error) { + return { success: false }; + } + + const distributionTags = data['dist-tags']; + const versionArray = Object.values(distributionTags); + const greatestVersion = versionArray.reduce((max, current) => { + return semver.gt(current, max) ? current : max; + }); + + return greatestVersion; + } catch (error) { + console.error('error:', error); + throw new Error('Not possible to get remote greatest version'); + } +}; + +const nonReleaseDependencies = []; + +const checkNonReleasedDependencies = async packageName => { + const rawPackageJSON = await readFile(path.join(ROOT, 'packages', packageName, 'package.json')); + + const packageJSON = JSON.parse(rawPackageJSON); + const { + version, + dependencies, + // devDependencies // We should ignore devDependencies. + } = packageJSON; + + const remoteGreatestVersion = await getNpmRemoteGreatestVersion(`@trezor/${packageName}`); + + // If local version is greatest than the greatest one in NPM we add it to the release. + if (semver.gt(version, remoteGreatestVersion)) { + const index = nonReleaseDependencies.indexOf(packageName); + if (index > -1) { + nonReleaseDependencies.splice(index, 1); + } + nonReleaseDependencies.push(packageName); + } + + if (!dependencies || !Object.keys(dependencies)) { + return; + } + + // eslint-disable-next-line no-restricted-syntax + for await (const [dependency] of Object.entries(dependencies)) { + // is not a dependency released from monorepo. we don't care + if (!dependency.startsWith('@trezor')) { + // eslint-disable-next-line no-continue + continue; + } + const [_prefix, name] = dependency.split('/'); + + console.log('name', name); + + await checkNonReleasedDependencies(name); + } + console.log('nonReleaseDependencies', nonReleaseDependencies); +}; + +const initConnectRelease = async () => { + // We check what dependencies need to be released because they have version bumped locally + // and remote greatest version is lower than the local one. + await checkNonReleasedDependencies('connect'); + await checkNonReleasedDependencies('connect-web'); + await checkNonReleasedDependencies('connect-webextension'); + console.log('Final nonReleaseDependencies', nonReleaseDependencies); + + // We use `nonReleaseDependencies` to trigger NPM releases + const dependenciesToRelease = JSON.stringify(nonReleaseDependencies); + console.log('dependenciesToRelease:', dependenciesToRelease); + console.log('deploymentType:', deploymentType); + console.log('branchName:', branchName); + + // Now we want to trigger the action that will trigger the actual release, + // after approval form authorized member. + const releaseDependencyActionOutput = triggerReleaseNpmWorkflow( + branchName, + dependenciesToRelease, + deploymentType, + ); + + console.log('releaseDependencyActionOutput output:', releaseDependencyActionOutput.stdout); +}; + +initConnectRelease(); diff --git a/ci/scripts/helpers.js b/ci/scripts/helpers.js index 7fef683d399f..86849ce7a62e 100644 --- a/ci/scripts/helpers.js +++ b/ci/scripts/helpers.js @@ -10,13 +10,6 @@ const readFile = util.promisify(fs.readFile); const { getLocalAndRemoteChecksums } = require('./check-npm-and-local'); -const rootPath = path.join(__dirname, '..', '..'); -const packagesPath = path.join(rootPath, 'packages'); - -const packages = fs.readdirSync(packagesPath, { - encoding: 'utf-8', -}); - const ROOT = path.join(__dirname, '..', '..'); const updateNeeded = [];