From e4aa1c658d388e04a25e96e6521f9eacf21184f9 Mon Sep 17 00:00:00 2001 From: Angel Campbell <151678097+angeldcampbell@users.noreply.github.com> Date: Wed, 30 Oct 2024 06:19:45 -0700 Subject: [PATCH 1/2] Refresh CTC workflow (#2802) * change component_name to app_id * remove api/ctc * Fixes to get job running * Clarify terminology in CTC workflow * Update required actions for Node 20 compat * Improve the lock retry timing * Check CTC before create-cli-release * Switch CTC check to use component name * Switch TPS integration to component_slug --------- Co-authored-by: Mars Hall --- .github/workflows/create-cli-release.yml | 4 ++ .github/workflows/ctc.yml | 63 +++++++++++++----------- scripts/postrelease/change_management | 2 +- 3 files changed, 39 insertions(+), 30 deletions(-) diff --git a/.github/workflows/create-cli-release.yml b/.github/workflows/create-cli-release.yml index e2e80a594b..380eea14f4 100644 --- a/.github/workflows/create-cli-release.yml +++ b/.github/workflows/create-cli-release.yml @@ -17,6 +17,10 @@ on: default: false jobs: + check-for-moratorium: + if: fromJSON(inputs.isStableCandidate) + uses: ./.github/workflows/ctc.yml + get-version-channel: runs-on: ubuntu-latest outputs: diff --git a/.github/workflows/ctc.yml b/.github/workflows/ctc.yml index 53f80352e5..301195f18e 100644 --- a/.github/workflows/ctc.yml +++ b/.github/workflows/ctc.yml @@ -5,54 +5,59 @@ on: workflow_call: jobs: - create-ctc-lock: + # Use TPS service to get clearance to release from Salesforce Change-Traffic-Control: + # https://github.com/heroku/tps/blob/master/docs/ctc.md + check-tps-for-lock: runs-on: ubuntu-latest environment: ChangeManagement steps: - - uses: actions/checkout@v3 - - name: Call CTC API TO Create Lock - id: create-lock + - uses: actions/checkout@v4 + - name: Check tps.heroku.tools for lock + id: check-lock run: | - CODE=`curl --w '%{http_code}' \ - -X PUT \ - -H "Accept: application/json" \ - -H "Content-Type: application/json" \ - -H "Authorization: Token ${{ secrets.TPS_API_TOKEN_PARAM }}" \ - -d '{"lock": {"sha": "${{ github.sha }}", "component_name": "${{ secrets.TPS_API_APP_ID }}"}}' \ - ${{ secrets.TPS_API_URL_PARAM }}/api/ctc` - echo "STATUS_CODE=$CODE" >> $GITHUB_ENV - echo "Response status code is $CODE." + STATUS_CODE="$(curl --w '%{http_code}' \ + -X PUT \ + -H "Accept: application/json" \ + -H "Content-Type: application/json" \ + -H "Authorization: Token ${{ secrets.TPS_API_TOKEN_PARAM }}" \ + -d '{"lock": {"sha": "${{ github.sha }}", "component_slug": "cli"}}' \ + https://tps.heroku.tools/api/ctc)" + + echo "Response status code is $STATUS_CODE." + echo "STATUS_CODE=$STATUS_CODE" >> $GITHUB_ENV - - name: Retry if TPS returns 409 + - name: Retry if lock cannot be obtained + id: retry-lock env: RETRY_LATER: "409" uses: - nick-fields/retry@v2 + nick-fields/retry@v3 if: ${{ env.STATUS_CODE == env.RETRY_LATER}} with: - max_attempts: 15 + max_attempts: 12 warning_on_retry: true - retry_wait_seconds: 3600 + retry_wait_seconds: 300 retry_on_exit_code: 1 - timeout_minutes: 3600 + timeout_seconds: 60 command: | - CODE=`curl --w '%{http_code}' \ - -X PUT \ - -H "Accept: application/json" \ - -H "Content-Type: application/json" \ - -H "Authorization: Token ${{ secrets.TPS_API_TOKEN_PARAM }}" \ - -d '{"lock": {"sha": "${{ github.sha }}", "component_name": "${{ secrets.TPS_API_APP_ID }}"}}' \ - ${{ secrets.TPS_API_URL_PARAM }}/api/ctc` + STATUS_CODE="$(curl --w '%{http_code}' \ + -X PUT \ + -H "Accept: application/json" \ + -H "Content-Type: application/json" \ + -H "Authorization: Token ${{ secrets.TPS_API_TOKEN_PARAM }}" \ + -d '{"lock": {"sha": "${{ github.sha }}", "component_slug": "cli"}}' \ + https://tps.heroku.tools/api/ctc)" - echo "Response status code is $CODE" - if [ $CODE == "409" ] + echo "Response status code is $STATUS_CODE" + if [ $STATUS_CODE == "409" ] then exit 1 else - echo "STATUS_CODE=$CODE" >> $GITHUB_ENV + echo "STATUS_CODE=$STATUS_CODE" >> $GITHUB_ENV fi - - name: Verify CTC Lock Did Not Fail for Other Reasons + - name: Verify lock status + id: verify-lock env: UPDATE_LOCK_SUCCESS: "200" NEW_LOCK_SUCCESS: "201" diff --git a/scripts/postrelease/change_management b/scripts/postrelease/change_management index e4b8a916e9..c1de90b915 100755 --- a/scripts/postrelease/change_management +++ b/scripts/postrelease/change_management @@ -46,7 +46,7 @@ async function sendDeployNotification () { release: { actor_email: actorEmail, app_id: appId, - component_name: 'cli', + component_slug: 'cli', description: `Deploy ${sha} of heroku/cli in ${stage}`, sha, stage From 80ab352a5b68e96dba6d5a0ed900f6e1c166d98f Mon Sep 17 00:00:00 2001 From: Santiago Bosio Date: Wed, 30 Oct 2024 10:47:04 -0300 Subject: [PATCH 2/2] feat(cli): Adding disclaimer for plugin AI install (#3065) * Adding a prerun hook to show AI plugin installation disclaimer * Moving logic to a plugins:preinstall hook * Adding tests for the feature * Updating conditional and adding tests for repo use case * Adding requested prompt --- cspell-dictionary.txt | 1 + packages/cli/package.json | 3 + .../hooks/plugins/preinstall/disclaimers.ts | 30 ++++ .../test/unit/hooks/disclaimers.unit.test.ts | 129 ++++++++++++++++++ 4 files changed, 163 insertions(+) create mode 100644 packages/cli/src/hooks/plugins/preinstall/disclaimers.ts create mode 100644 packages/cli/test/unit/hooks/disclaimers.unit.test.ts diff --git a/cspell-dictionary.txt b/cspell-dictionary.txt index 573bccbd71..67a797be21 100644 --- a/cspell-dictionary.txt +++ b/cspell-dictionary.txt @@ -286,6 +286,7 @@ searchpath secrettoken segv selfsigned +sfdc shellescape showenvs sigints diff --git a/packages/cli/package.json b/packages/cli/package.json index b776ce9a21..79c5e9928b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -317,6 +317,9 @@ "./lib/hooks/init/terms-of-service", "./lib/hooks/init/performance_analytics" ], + "plugins:preinstall": [ + "./lib/hooks/plugins/preinstall/disclaimers" + ], "prerun": [ "./lib/hooks/prerun/analytics" ], diff --git a/packages/cli/src/hooks/plugins/preinstall/disclaimers.ts b/packages/cli/src/hooks/plugins/preinstall/disclaimers.ts new file mode 100644 index 0000000000..1aff04b5ce --- /dev/null +++ b/packages/cli/src/hooks/plugins/preinstall/disclaimers.ts @@ -0,0 +1,30 @@ +import {Hook, ux} from '@oclif/core' + +const hook: Hook<'plugins:preinstall'> = async function (options) { + const npmPackageNames = ['@heroku/plugin-ai', '@heroku-cli/plugin-ai'] + + if (options.plugin.type !== 'npm' || !npmPackageNames.includes(options.plugin.name)) return + + ux.warn( + '\n\nThis pilot feature is a Beta Service. You may opt to try such Beta Service in your sole discretion. ' + + 'Any use of the Beta Service is subject to the applicable Beta Services Terms provided at ' + + 'https://www.salesforce.com/company/legal/customer-agreements/. While use of the pilot feature itself is free, ' + + 'to the extent such use consumes a generally available Service, you may be charged for that consumption as set ' + + 'forth in the Documentation. Your continued use of this pilot feature constitutes your acceptance of the foregoing.\n\n' + + 'For clarity and without limitation, the various third-party machine learning and generative artificial intelligence ' + + '(AI) models and applications (each a “Platform”) integrated with the Beta Service are Non-SFDC Applications, ' + + 'as that term is defined in the Beta Services Terms. Note that these third-party Platforms include features that use ' + + 'generative AI technology. Due to the nature of generative AI, the output that a Platform generates may be ' + + 'unpredictable, and may include inaccurate or harmful responses. Before using any generative AI output, Customer is ' + + 'solely responsible for reviewing the output for accuracy, safety, and compliance with applicable laws and third-party ' + + 'acceptable use policies. In addition, Customer’s use of each Platform may be subject to the Platform’s own terms and ' + + 'conditions, compliance with which Customer is solely responsible.\n', + ) + + const response = await ux.prompt('Continue? (Y/N)') + if (response.toUpperCase() !== 'Y') { + ux.error('Canceled', {exit: 1}) + } +} + +export default hook diff --git a/packages/cli/test/unit/hooks/disclaimers.unit.test.ts b/packages/cli/test/unit/hooks/disclaimers.unit.test.ts new file mode 100644 index 0000000000..0fa70074e9 --- /dev/null +++ b/packages/cli/test/unit/hooks/disclaimers.unit.test.ts @@ -0,0 +1,129 @@ +import {Config, ux} from '@oclif/core' +import {CLIError} from '@oclif/core/lib/errors' +import {expect} from 'chai' +import {join} from 'path' +import * as sinon from 'sinon' +import {stderr} from 'stdout-stderr' + +describe('disclaimers ‘plugins:preinstall’ hook', function () { + let config: Config + let sandbox: sinon.SinonSandbox + + before(async function () { + config = await Config.load({root: join(__dirname, '../../..')}) + sandbox = sinon.createSandbox() + }) + + afterEach(function () { + sandbox.restore() + }) + + context('when installing from a Github repository', function () { + it('doesn’t show the disclaimer', async function () { + stderr.start() + config.runHook('plugins:preinstall', { + plugin: { + url: 'https://github.com/heroku/heroku-api-plugin', + type: 'repo', + }, + }) + stderr.stop() + + expect(stderr.output).not.to.include('This pilot feature is a Beta Service.') + }) + }) + + context('when installing a plugin different from ‘@heroku/plugin-ai’ or ‘@heroku-cli/plugin-ai’', function () { + it('doesn’t show the disclaimer', async function () { + stderr.start() + config.runHook('plugins:preinstall', { + plugin: { + name: '@heroku-cli/plugin-events', + tag: 'latest', + type: 'npm', + }, + }) + stderr.stop() + + expect(stderr.output).not.to.include('This pilot feature is a Beta Service.') + }) + }) + + context('when installing the ‘@heroku/plugin-ai’ plugin', function () { + it('shows the disclaimer and prompts the user', async function () { + const promptStub = sandbox.stub(ux, 'prompt').onFirstCall().resolves('y') + + stderr.start() + await config.runHook('plugins:preinstall', { + plugin: { + name: '@heroku/plugin-ai', + tag: 'latest', + type: 'npm', + }, + }) + stderr.stop() + + expect(stderr.output).to.include('This pilot feature is a Beta Service.') + expect(promptStub.calledOnce).to.be.true + }) + + it('cancels installation if customer doesn’t accepts the prompt', async function () { + sandbox.stub(ux, 'prompt').onFirstCall().resolves('n') + + stderr.start() + try { + await config.runHook('plugins:preinstall', { + plugin: { + name: '@heroku/plugin-ai', + tag: 'latest', + type: 'npm', + }, + }) + } catch (error: unknown) { + stderr.stop() + const {message, oclif} = error as CLIError + expect(message).to.equal('Canceled') + expect(oclif.exit).to.equal(1) + } + }) + }) + + context('when installing the ‘@heroku-cli/plugin-ai’ plugin', function () { + it('shows the disclaimer and prompts the user', async function () { + const promptStub = sandbox.stub(ux, 'prompt').onFirstCall().resolves('y') + + stderr.start() + await config.runHook('plugins:preinstall', { + plugin: { + name: '@heroku-cli/plugin-ai', + tag: 'latest', + type: 'npm', + }, + }) + stderr.stop() + + expect(stderr.output).to.include('This pilot feature is a Beta Service.') + expect(promptStub.calledOnce).to.be.true + }) + + it('cancels installation if customer doesn’t accepts the prompt', async function () { + sandbox.stub(ux, 'prompt').onFirstCall().resolves('n') + + stderr.start() + try { + await config.runHook('plugins:preinstall', { + plugin: { + name: '@heroku-cli/plugin-ai', + tag: 'latest', + type: 'npm', + }, + }) + } catch (error: unknown) { + stderr.stop() + const {message, oclif} = error as CLIError + expect(message).to.equal('Canceled') + expect(oclif.exit).to.equal(1) + } + }) + }) +})