diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml new file mode 100644 index 000000000..1bb2dd995 --- /dev/null +++ b/.github/workflows/pre-release.yml @@ -0,0 +1,87 @@ +--- +# When triggered it will prepare the release by creating a Pull Request with the +# changes to be merged. Then it will be possible to run the release manually +name: pre-release + +on: + workflow_dispatch: + inputs: + dry-run: + type: boolean + description: 'Run release process in dry-run mode' + default: true + +permissions: + contents: read + +env: + SLACK_BUILD_MESSAGE: "Build: (<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|here>)" + +jobs: + release: + runs-on: ubuntu-latest + permissions: + # Needed to write the release changelog + contents: write + steps: + - name: Configure github token + uses: elastic/apm-pipeline-library/.github/actions/github-token@current + with: + url: ${{ secrets.VAULT_ADDR }} + roleId: ${{ secrets.VAULT_ROLE_ID }} + secretId: ${{ secrets.VAULT_SECRET_ID }} + + - name: Configure git user + uses: elastic/apm-pipeline-library/.github/actions/setup-git@current + with: + username: ${{ env.GIT_USER }} + email: ${{ env.GIT_EMAIL }} + token: ${{ env.GITHUB_TOKEN }} + + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + token: ${{ env.GITHUB_TOKEN }} + + - uses: actions/setup-node@v3 + with: + node-version-file: '.nvmrc' + + - name: Install dependencies + run: npm ci + + # This prevent lerna command from throwing this error: + # "Working tree has uncommitted changes, please commit or remove the following changes before continuing" + - name: Ignore git uncommitted changes + run: git update-index --skip-worktree .npmrc + + - name: Create PR with the required changes to be released + id: pre-release + env: + DRY_RUN: "${{ inputs.dry-run }}" + run: | + npm run ci:pre-release + if [ -e .pr.txt ] ; then + echo "pr=$(cat .pr.txt)" >> "$GITHUB_OUTPUT" + rm .pr.txt + fi + + - if: ${{ success() && inputs.dry-run == false }} + uses: elastic/apm-pipeline-library/.github/actions/slack-message@current + with: + url: ${{ secrets.VAULT_ADDR }} + roleId: ${{ secrets.VAULT_ROLE_ID }} + secretId: ${{ secrets.VAULT_SECRET_ID }} + channel: "#apm-agent-js" + message: | + :runner: [${{ github.repository }}] Pre Release has been triggered. Review the PR ${{ steps.pre-release.outputs.pr }}. ${{ env.SLACK_BUILD_MESSAGE }} + + - if: ${{ failure() && inputs.dry-run == false }} + uses: elastic/apm-pipeline-library/.github/actions/slack-message@current + with: + url: ${{ secrets.VAULT_ADDR }} + roleId: ${{ secrets.VAULT_ROLE_ID }} + secretId: ${{ secrets.VAULT_SECRET_ID }} + channel: "#apm-agent-js" + message: | + :ghost: [${{ github.repository }}] Pre Release failed. ${{ env.SLACK_BUILD_MESSAGE }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8703c1edb..bb4d5f7d1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,3 +1,6 @@ +--- +# When triggered it will publish the release only if the changes are related to the +# release name: release on: @@ -11,6 +14,11 @@ on: permissions: contents: read +env: + ELASTIC_CDN_BUCKET_NAME: ${{ inputs.dry-run == false && 'apm-rum-357700bc' || 'oblt-apm-agent-rum-js-ci' }} + ELASTIC_CDN_CREDENTIALS: ${{ inputs.dry-run == false && 'secret/gce/elastic-cdn/service-account/apm-rum-admin' || 'secret/observability-team/ci/service-account/apm-agent-rum-js' }} + SLACK_BUILD_MESSAGE: "Build: (<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|here>)" + jobs: release: runs-on: ubuntu-latest @@ -49,7 +57,7 @@ jobs: - name: Install dependencies run: npm ci - - name: Read NPM vault secrets + - name: Read NPM vault TOTP if: inputs.dry-run == false uses: hashicorp/vault-action@v2.7.3 with: @@ -67,8 +75,8 @@ jobs: git update-index --skip-worktree .npmrc - name: Configure npm registry - uses: elastic/apm-pipeline-library/.github/actions/setup-npmrc@current if: inputs.dry-run == false + uses: elastic/apm-pipeline-library/.github/actions/setup-npmrc@current with: vault-url: ${{ secrets.VAULT_ADDR }} vault-role-id: ${{ secrets.VAULT_ROLE_ID }} @@ -78,21 +86,9 @@ jobs: - name: Publish the release env: - DRY_RUN: ${{ inputs.dry-run }} + DRY_RUN: "${{ inputs.dry-run }}" run: npm run ci:release - - name: Setup credentials - env: - DRY_RUN: ${{ inputs.dry-run }} - run: | - if [ "${DRY_RUN}" == "false" ]; then - echo 'ELASTIC_CDN_BUCKET_NAME=apm-rum-357700bc' >> ${GITHUB_ENV} - echo 'ELASTIC_CDN_CREDENTIALS=secret/gce/elastic-cdn/service-account/apm-rum-admin' >> ${GITHUB_ENV} - else - echo 'ELASTIC_CDN_BUCKET_NAME=oblt-apm-agent-rum-js-ci' >> ${GITHUB_ENV} - echo 'ELASTIC_CDN_CREDENTIALS=secret/observability-team/ci/service-account/apm-agent-rum-js' >> ${GITHUB_ENV} - fi - - name: Read GCE vault secrets uses: hashicorp/vault-action@v2.7.3 with: @@ -148,22 +144,22 @@ jobs: headers: |- cache-control: public,max-age=604800,immutable - status: - if: always() - needs: - - release - runs-on: ubuntu-latest - steps: - - id: check - uses: elastic/apm-pipeline-library/.github/actions/check-dependent-jobs@current + - if: ${{ success() && inputs.dry-run == false }} + uses: elastic/apm-pipeline-library/.github/actions/slack-message@current with: - needs: ${{ toJSON(needs) }} - - uses: elastic/apm-pipeline-library/.github/actions/notify-build-status@current - if: inputs.dry-run == false + url: ${{ secrets.VAULT_ADDR }} + roleId: ${{ secrets.VAULT_ROLE_ID }} + secretId: ${{ secrets.VAULT_SECRET_ID }} + channel: "#apm-agent-js" + message: | + :runner: [${{ github.repository }}] Release has been published. ${{ env.SLACK_BUILD_MESSAGE }} + + - if: ${{ failure() && inputs.dry-run == false }} + uses: elastic/apm-pipeline-library/.github/actions/slack-message@current with: - status: ${{ steps.check.outputs.status }} - vaultUrl: ${{ secrets.VAULT_ADDR }} - vaultRoleId: ${{ secrets.VAULT_ROLE_ID }} - vaultSecretId: ${{ secrets.VAULT_SECRET_ID }} - slackChannel: "#apm-agent-js" - message: "Build result for release publication" + url: ${{ secrets.VAULT_ADDR }} + roleId: ${{ secrets.VAULT_ROLE_ID }} + secretId: ${{ secrets.VAULT_SECRET_ID }} + channel: "#apm-agent-js" + message: | + :ghost: [${{ github.repository }}] Release failed. ${{ env.SLACK_BUILD_MESSAGE }} diff --git a/RELEASE.md b/RELEASE.md index 4f6110344..db07c9f02 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -42,8 +42,12 @@ Before releasing, be sure to update the following documentation: The release process is also automated in the way any specific commit from the main branch can be potentially released, for such it's required the below steps: -* Go to the [GitHub Actions](https://github.com/elastic/apm-agent-rum-js/actions/workflows/release.yml) workflow. +* Go to the [GitHub Actions](https://github.com/elastic/apm-agent-rum-js/actions/workflows/pre-release.yml) workflow. * Click on `Run workflow` and select the `main` branch. * Click on `Run workflow`. * Wait for completion. +* Review the PR with the changes when merged +* Go to the [GitHub Actions](https://github.com/elastic/apm-agent-rum-js/actions/workflows/release.yml) workflow. +* Click on `Run workflow` and select the `main` branch. +* Click on `Run workflow`. * You can go to the `https://www.npmjs.com/package/@elastic/apm-rum` and [GitHub releases](https://github.com/elastic/apm-agent-rum-js/releases) to validate that the bundles and release notes have been published. diff --git a/lerna.json b/lerna.json index a08b1576d..e7b4e4656 100644 --- a/lerna.json +++ b/lerna.json @@ -6,7 +6,7 @@ "registry": "https://registry.npmjs.org", "command": { "version": { - "allowBranch": ["main", "4.x", "release"], + "allowBranch": ["main", "4.x", "release", "release/*"], "conventionalCommits": true, "message": "chore(release): publish", "changelogPreset": "conventionalcommits", diff --git a/package.json b/package.json index 68599be26..1a638b160 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "package:snapshot": "lerna exec npm pack", "clean": "lerna exec -- rm -rf dist/", "ci:prepare-release": "node ./scripts/ci-prepare-release.js", + "ci:pre-release": "node ./scripts/ci-pre-release.mjs", "ci:release": "node ./scripts/ci-release.mjs" }, "lint-staged": { diff --git a/scripts/ci-pre-release.mjs b/scripts/ci-pre-release.mjs new file mode 100644 index 000000000..9c851ed8a --- /dev/null +++ b/scripts/ci-pre-release.mjs @@ -0,0 +1,168 @@ +/** + * MIT License + * + * Copyright (c) 2017-present, Elasticsearch BV + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +import { execa } from 'execa' +import * as process from 'node:process' +// To read the version then use https://nodejs.org/api/esm.html#no-require-exports-or-moduleexports +import { createRequire } from 'node:module' +const require = createRequire(import.meta.url) +const { version } = require('../packages/rum/package.json') + +function raiseError(msg) { + console.log(msg) + process.exit(1) +} + +async function gitContext() { + try { + const { stdout: username } = await execa('git', ['config', 'user.name']) + const { stdout: email } = await execa('git', ['config', 'user.email']) + return { + username, + email + } + } catch (err) { + raiseError('Failed to extract git context') + } +} + +// Script logic +async function main() { + const isDryRun = + process.env.DRY_RUN == null || process.env.DRY_RUN !== 'false' + + // Extract git context + const ctx = await gitContext() + console.log(`Git User: username=${ctx.username}, email=${ctx.email}`) + + if (isDryRun) { + await dryRunMode() + } else { + await prodMode() + } +} + +// Script logic +async function dryRunMode() { + console.log('Running in dry-run mode') + + const githubToken = process.env.GITHUB_TOKEN + if (githubToken == null || githubToken === '') { + raiseError("The 'GITHUB_TOKEN' env var isn't defined") + } + + const branch = `release/${version}-next` + + try { + await execa('git', ['checkout', '-b', branch], { + stdin: process.stdin + }) + .pipeStdout(process.stdout) + .pipeStderr(process.stderr) + } catch (err) { + raiseError('Failed to create git branch') + } + + try { + await execa('npx', ['lerna', 'version', '--no-push', '--no-git-tag-version', '--no-changelog', 'yes'], { + stdin: process.stdin, + env: { + GH_TOKEN: githubToken + } + }) + .pipeStdout(process.stdout) + .pipeStderr(process.stderr) + } catch (err) { + raiseError('Failed to version npm') + } +} + +async function prodMode() { + console.log('Running in prod mode') + + const githubToken = process.env.GITHUB_TOKEN + if (githubToken == null || githubToken === '') { + raiseError("The 'GITHUB_TOKEN' env var isn't defined") + } + + const branch = `release/${version}-next` + + try { + await execa('git', ['checkout', '-b', branch], { + stdin: process.stdin + }) + .pipeStdout(process.stdout) + .pipeStderr(process.stderr) + } catch (err) { + raiseError('Failed to create git branch') + } + + try { + await execa('npx', ['lerna', 'version', '--yes', '--no-push'], { + stdin: process.stdin, + env: { + GH_TOKEN: githubToken + } + }) + .pipeStdout(process.stdout) + .pipeStderr(process.stderr) + } catch (err) { + raiseError('Failed to version npm') + } + + // As long as lerna version uses --no-push then it's required to push the commits + // this will avoid pushing the git tag too. + try { + await execa('git', ['push', 'origin', branch], { + stdin: process.stdin + }) + .pipeStdout(process.stdout) + .pipeStderr(process.stderr) + } catch (err) { + raiseError('Failed to push git branch') + } + + try { + await execa('gh', ['pr', 'create', '--fill-first'], { + stdin: process.stdin, + env: { + GH_TOKEN: githubToken + } + }) + .pipeStdout('.pr.txt') + .pipeStderr(process.stderr) + } catch (err) { + raiseError('Failed to create GitHub PR') + } +} + +// Entrypoint +;(async () => { + try { + await main() + } catch (err) { + console.log(err) + } +})() diff --git a/scripts/ci-release.mjs b/scripts/ci-release.mjs index 1b1e10c05..fc919e435 100644 --- a/scripts/ci-release.mjs +++ b/scripts/ci-release.mjs @@ -108,7 +108,7 @@ async function dryRunMode() { try { await execa( 'npx', - ['lerna', 'publish', `--registry=${registryUrl}`, '--no-push', '--yes'], + ['lerna', 'publish', 'from-package', `--registry=${registryUrl}`, '--no-push', '--no-git-tag-version', '--no-changelog', '--yes'], { stdin: process.stdin } ) .pipeStdout(process.stdout) @@ -140,12 +140,10 @@ async function prodMode() { } try { - await execa('npx', ['lerna', 'publish', `--otp=${totpCode}`, '--yes'], { - stdin: process.stdin, - env: { - GH_TOKEN: githubToken - } - }) + await execa('npx', + ['lerna', 'publish', 'from-package', `--otp=${totpCode}`, '--no-push', '--no-git-tag-version', '--no-changelog', '--yes'], + { stdin: process.stdin} + ) .pipeStdout(process.stdout) .pipeStderr(process.stderr) } catch (err) {