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..10d742261438 --- /dev/null +++ b/.github/workflows/release-connect-init.yml @@ -0,0 +1,98 @@ +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 + deploymentType: + description: "Select the deployment type. (example: canary, stable)" + required: true + type: choice + options: + - canary + - stable + +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-v9-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-v9-production-release: + runs-on: ubuntu-latest + steps: + - run: gh workflow run .github/workflows/release-connect-v9-production.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 --field deploymentType=${{ github.event.inputs.deploymentType }} + 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..58d465784b61 100644 --- a/.github/workflows/release-connect-npm-init.yml +++ b/.github/workflows/release-connect-npm-init.yml @@ -3,21 +3,50 @@ name: "[Release] connect npm init" on: workflow_dispatch: inputs: - semver: + deploymentType: + description: "Select the deployment type. (example: canary, stable)" + required: true type: choice - description: semver options: - - patch - - minor - - prerelease + - canary + - stable concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: - pre-release: - if: github.repository == 'trezor/trezor-suite' + 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-connect-version-match.yml + with: + branch_ref: "${{ github.ref }}" + extracted_version: "${{ needs.extract-version.outputs.version }}" + + trigger-npm-release: + needs: [check-version-match] + if: startsWith(github.ref, 'refs/heads/release/connect/') runs-on: ubuntu-latest steps: - name: Checkout @@ -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.deploymentType }} release/connect/${{ needs.extract-version.outputs.version }} env: GITHUB_TOKEN: ${{ secrets.TREZOR_BOT_TOKEN }} diff --git a/.github/workflows/release-connect-npm.yml b/.github/workflows/release-connect-npm.yml index 3bd898dff26b..036f0caed760 100644 --- a/.github/workflows/release-connect-npm.yml +++ b/.github/workflows/release-connect-npm.yml @@ -16,6 +16,7 @@ on: jobs: deploy-npm: + name: Deploy NPM ${{ inputs.deploymentType }} environment: production-connect runs-on: ubuntu-latest strategy: diff --git a/.github/workflows/release-connect-v9-poduction.yml b/.github/workflows/release-connect-v9-production.yml similarity index 87% rename from .github/workflows/release-connect-v9-poduction.yml rename to .github/workflows/release-connect-v9-production.yml index c91d6bc46596..a88519e93e44 100644 --- a/.github/workflows/release-connect-v9-poduction.yml +++ b/.github/workflows/release-connect-v9-production.yml @@ -36,8 +36,17 @@ 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-connect-version-match.yml + with: + branch_ref: "${{ github.ref }}" + extracted_version: "${{ needs.extract-version.outputs.version }}" + # set the rollback sync-rollback-connect-v9: + needs: [extract-version, check-version-match] if: startsWith(github.ref, 'refs/heads/release/connect/') environment: production-connect name: "Backing up current production version ${{ needs.extract-version.outputs.version }} to rollback bucket" @@ -48,7 +57,7 @@ jobs: - name: Configure aws credentials uses: aws-actions/configure-aws-credentials@v4 with: - role-to-assume: arn:aws:iam::538326561891:role/gh_actions_trezor_suite_prod_deploy + role-to-assume: arn:aws:iam::538326561891:role/gh_actions_connect_prod_deploy aws-region: eu-central-1 - name: Synching rollback bucket with current production @@ -56,6 +65,7 @@ jobs: aws s3 sync "s3://connect.trezor.io/${{ env.LATEST_VERSION }}/" "s3://rollback-connect.trezor.io/${{ env.LATEST_VERSION }}/" deploy-production-semantic-version: + needs: [extract-version, check-version-match] if: startsWith(github.ref, 'refs/heads/release/connect/') environment: production-connect name: "Deploying to connect.trezor.io/9.x.x" @@ -66,7 +76,7 @@ jobs: - name: Configure aws credentials uses: aws-actions/configure-aws-credentials@v4 with: - role-to-assume: arn:aws:iam::538326561891:role/gh_actions_trezor_suite_prod_deploy + role-to-assume: arn:aws:iam::538326561891:role/gh_actions_connect_prod_deploy aws-region: eu-central-1 - name: Synching production bucket with current staging @@ -77,7 +87,7 @@ jobs: # From staging move it to production deploy-production-v9: # We deploy to production only if rollback sync was successful. - needs: [sync-rollback-connect-v9] + needs: [extract-version, check-version-match, sync-rollback-connect-v9] if: startsWith(github.ref, 'refs/heads/release/connect/') && github.event.inputs.deploymentType == 'stable' environment: production-connect name: "Deploying to connect.trezor.io/9/" diff --git a/.github/workflows/release-connect-v9-staging.yml b/.github/workflows/release-connect-v9-staging.yml index 1d126c52bb39..765b62698eea 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-connect-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: @@ -68,7 +58,7 @@ jobs: - name: Build and deploy to staging-connect.trezor.io/9.x.x uses: ./.github/actions/release-connect with: - awsRoleToAssume: "arn:aws:iam::538326561891:role/gh_actions_trezor_suite_prod_deploy" + awsRoleToAssume: "arn:aws:iam::538326561891:role/gh_actions_connect_staging_deploy" awsRegion: "eu-central-1" serverHostname: "staging-connect.trezor.io" serverPath: ${{ needs.extract-version.outputs.version }} @@ -94,7 +84,7 @@ jobs: - name: Build and deploy to staging-connect.trezor.io/9 uses: ./.github/actions/release-connect with: - awsRoleToAssume: "arn:aws:iam::538326561891:role/gh_actions_trezor_suite_prod_deploy" + awsRoleToAssume: "arn:aws:iam::538326561891:role/gh_actions_connect_staging_deploy" awsRegion: "eu-central-1" serverHostname: "staging-connect.trezor.io" serverPath: "9" diff --git a/.github/workflows/template-check-connect-version-match.yml b/.github/workflows/template-check-connect-version-match.yml new file mode 100644 index 000000000000..5bcc45276acc --- /dev/null +++ b/.github/workflows/template-check-connect-version-match.yml @@ -0,0 +1,37 @@ +name: Check connect version match with branch + +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..e210e621a802 --- /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: deploymentType branchName'); +const [deploymentType, branchName] = args; + +const allowedDeploymentType = ['canary', 'stable']; +if (!allowedDeploymentType.includes(deploymentType)) { + throw new Error( + `provided semver: ${deploymentType} must be one of ${allowedDeploymentType.join(', ')}`, + ); +} + +const readFile = util.promisify(fs.readFile); + +const ROOT = path.join(__dirname, '..', '..'); + +const triggerReleaseNpmWorkflow = (branch, packages, type) => + exec('gh', [ + 'workflow', + 'run', + '.github/workflows/release-connect-npm.yml', + '--ref', + branch, + '--field', + `packages=${packages}`, + '--field', + `deploymentType=${type}`, + ]); + +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 initConnectNpmRelease = 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); +}; + +initConnectNpmRelease(); 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 = [];