From 4643ce6227d77e2c42f2011d533b4ede90254a4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Enrique=20Colina=20Rodr=C3=ADguez?= Date: Tue, 23 Apr 2024 17:57:05 +0200 Subject: [PATCH] feat(CICD): #27805 Adding NPM publishing to nightly deployment workflow. (#28248) * #27805 Adding NPM publishing to nightly deployment workflow. * #27805 Fixing the path of the download artifact step. * #27805 Refactor on publish npm cli workflow. * #27805 Fixing trigger conditional for publishing NPM package. * #27805 Find a previous build if the current is missing. --------- Co-authored-by: Daniel Colina --- .github/actions/publish-npm-cli/README.md | 85 +++++++ .github/actions/publish-npm-cli/action.yml | 275 +++++++++++++++++++++ .github/workflows/build-test-nightly.yml | 7 + .github/workflows/publish-npm-package.yml | 75 ------ .github/workflows/reusable-deployment.yml | 19 +- 5 files changed, 385 insertions(+), 76 deletions(-) create mode 100644 .github/actions/publish-npm-cli/README.md create mode 100644 .github/actions/publish-npm-cli/action.yml delete mode 100644 .github/workflows/publish-npm-package.yml diff --git a/.github/actions/publish-npm-cli/README.md b/.github/actions/publish-npm-cli/README.md new file mode 100644 index 000000000000..2cb0d2752304 --- /dev/null +++ b/.github/actions/publish-npm-cli/README.md @@ -0,0 +1,85 @@ +# CLI Publish NPM Package Action + +## Description + +This GitHub Action workflow packages the dotCMS CLI as an NPM project. + +## Inputs + +### `ref` + +Branch to build from. + +- **Description**: Branch to build the project from. +- **Required**: No +- **Default**: `master` + +### `github-token` + +GitHub Token. + +- **Description**: GitHub Token required to access GitHub resources. +- **Required**: Yes + +### `npm-token` + +NPM Token. + +- **Description**: NPM Token required to publish the package to the NPM registry. +- **Required**: Yes + +### `npm-package-name` + +NPM package name. + +- **Description**: NPM package name to be published. +- **Required**: No +- **Default**: `dotcli` + +### `npm-package-scope` + +NPM package scope. + +- **Description**: NPM package scope. +- **Required**: No +- **Default**: `@dotcms` + +### `node-version` + +Node.js version. + +- **Description**: Node.js version to be used to build the project. +- **Required**: No +- **Default**: `19` + +### `workflow-to-search` + +Workflow to search for artifacts. + +- **Description**: Name of the workflow to search for to get the artifacts. +- **Required**: No +- **Default**: `build-test-master.yml` + +### `artifact-id` + +Artifact id. + +- **Description**: Identifier of the artifact to be searched for and downloaded. +- **Required**: No +- **Default**: `cli-artifacts-*` + +## Usage + +```yaml +- name: CLI Publish NPM Package + uses: ./.github/actions/publish-npm-cli + with: + ref: 'master' + github-token: ${{ secrets.GITHUB_TOKEN }} + npm-token: ${{ secrets.NPM_TOKEN }} + npm-package-name: 'dotcli' + npm-package-scope: '@dotcms' + node-version: '19' + workflow-to-search: 'build-test-main.yml' + artifact-id: 'cli-artifacts-*' +``` \ No newline at end of file diff --git a/.github/actions/publish-npm-cli/action.yml b/.github/actions/publish-npm-cli/action.yml new file mode 100644 index 000000000000..407761ba2a7e --- /dev/null +++ b/.github/actions/publish-npm-cli/action.yml @@ -0,0 +1,275 @@ +name: 'CLI Publish NPM Package' +description: 'Package the dotCMS CLI as an NPM project.' +inputs: + ref: + description: 'Branch to build from' + required: false + default: 'master' + github-token: + description: 'GitHub Token' + required: true + npm-token: + description: 'NPM Token' + required: true + npm-package-name: + description: 'NPM package name' + required: false + default: 'dotcli' + npm-package-scope: + description: 'NPM package scope' + required: false + default: '@dotcms' + node-version: + description: 'Node.js version' + required: false + default: '19' + artifact-id: + description: 'Artifact id' + required: false + default: 'cli-artifacts-*' + reuse-previous-build: + description: 'Indicates if the workflow should reuse the previous build' + required: false + +runs: + using: "composite" + steps: + - name: 'Checkout' + uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + + - name: 'Set up Node.js' + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + + - name: 'Install Jinja2' + run: pip install jinja2-cli + shell: bash + + - name: 'Download Build Artifact' + id: data-download + uses: dawidd6/action-download-artifact@v3.1.4 + with: + github_token: ${{ inputs.github-token }} + #workflow: ${{ inputs.workflow-to-search }} + commit: ${{ github.sha }} + workflow_conclusion: success + search_artifacts: true + dry_run: true + name: ${{ inputs.artifact-id }} + name_is_regexp: true + path: . + if_no_artifact_found: warn + + - name: 'Check if artifact exists' + id: check + run: | + build_artifact_exists=${{ steps.data-download.outputs.found_artifact }} + if [[ ${build_artifact_exists} == "true" ]]; then + run_id=`echo '${{ steps.data-download.outputs.artifacts }}' | jq -r '.[0].workflow_run.id'` + found_artifacts=true + echo "Artifact Run id: $run_id" + else + echo "No artifact found" + run_id="${{ github.run_id }}" + found_artifacts=false + fi + echo "run_id=$run_id" >> $GITHUB_OUTPUT + echo "found_artifacts=$found_artifacts" >> $GITHUB_OUTPUT + shell: bash + + - name: Download Previous Build Artifact + id: data-download-previous + uses: dawidd6/action-download-artifact@v3.1.4 + if: ${{ inputs.reuse-previous-build == true && steps.data-download.outputs.found_artifact == 'false' }} + with: + github_token: ${{ inputs.github-token }} + #workflow: build-test-merge_group.yml + workflow_search: true + #commit: ${{ github.sha }} + workflow_conclusion: success + search_artifacts: true + dry_run: true + name: ${{ inputs.artifact-id }} + path: . + if_no_artifact_found: warn + + - name: 'Check if any previous artifact exists' + id: check-previous + if: ${{ inputs.reuse-previous-build == true && steps.data-download.outputs.found_artifact == 'false' }} + run: | + build_artifact_exists=${{ steps.data-download-previous.outputs.found_artifact }} + if [[ ${build_artifact_exists} == "true" ]]; then + run_id=`echo '${{ steps.data-download-previous.outputs.artifacts }}' | jq -r '.[0].workflow_run.id'` + found_artifacts=true + echo "Artifact Run id: $run_id" + else + echo "No artifact found" + run_id="${{ github.run_id }}" + found_artifacts=false + fi + echo "run_id=$run_id" >> $GITHUB_OUTPUT + echo "found_artifacts=$found_artifacts" >> $GITHUB_OUTPUT + shell: bash + + - name: 'Download all build artifacts' + id: download-cli-artifacts + uses: actions/download-artifact@v4 + with: + pattern: ${{ inputs.artifact-id }} + path: ${{ github.workspace }}/artifacts + github-token: ${{ inputs.github-token }} # token with actions:read permissions on target repo + merge-multiple: true + run-id: ${{ steps.check.outputs.run_id }} + + - name: 'List CLI Artifacts' + run: | + echo "::group::CLI Artifacts" + echo "Artifacts" + ls -R ${{ github.workspace }}/artifacts + echo "::endgroup::" + shell: bash + + - name: 'Extract run-id' + id: extract-metadata + run: | + echo "run-id=${{ github.run_id }}" >> $GITHUB_OUTPUT + shell: bash + + - name: 'Extract package version' + id: project + run: | + echo "::group::Extract package version" + version=$(./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout -pl :dotcms-cli) + echo "::debug::PROJECT VERSION: $version" + echo "version=$version" >> $GITHUB_OUTPUT + echo "::endgroup::" + shell: bash + # Determines the NPM package version and tag + # Distinguishes between snapshots and releases + + - name: 'Dynamic configuration of NPM package Version and Tag' + id: npm-version-tag + run: | + echo "::group::NPM package Version and Tag" + MVN_PACKAGE_VERSION=$(echo ${{ steps.project.outputs.version }} | tr '[:lower:]' '[:upper:]') + PACKAGE_FULL_NAME=${{ inputs.npm-package-scope }}/${{ inputs.npm-package-name }} + + # Check if the npm package exists + if ! npm view $PACKAGE_FULL_NAME &> /dev/null; then + echo "::error::The package $PACKAGE_FULL_NAME does not exist on npm." + exit 1 + fi + + # Check if the package is a snapshot + REGEX="([0-9]+\.[0-9]+\.[0-9]+)-SNAPSHOT" + + if [[ $MVN_PACKAGE_VERSION =~ $REGEX ]]; then + echo "::debug::Snapshot version found." + + NPM_PACKAGE_VERSION_TAG="rc" + MVN_BASE_VERSION="${BASH_REMATCH[1]}" + + # Use regular expression to extract version components + if [[ $MVN_BASE_VERSION =~ ([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then + MAJOR=$(echo "${BASH_REMATCH[1]}" | sed "s/\b0\+\([1-9]\)/\1/g") + MINOR=$(echo "${BASH_REMATCH[2]}" | sed "s/\b0\+\([1-9]\)/\1/g") + PATCH=$(echo "${BASH_REMATCH[3]}" | sed "s/\b0\+\([1-9]\)/\1/g") + VERSION_NPM_FORMAT="${MAJOR}.${MINOR}.${PATCH}" + + echo "::debug::VERSION_NPM_FORMAT: ${VERSION_NPM_FORMAT}" + else + echo "::error::Invalid Maven version format: $MVN_BASE_VERSION" + exit 1 + fi + + echo "VERSION_NPM_FORMAT: ${VERSION_NPM_FORMAT}" + LAST_RC_VERSIONS=$(npm view $PACKAGE_FULL_NAME versions --json) + echo "LAST_RC_VERSIONS: ${LAST_RC_VERSIONS}" + + LAST_RC_VERSION=$(npm view $PACKAGE_FULL_NAME versions --json | jq --arg filter $VERSION_NPM_FORMAT 'map(.| select(. | contains($filter)))' | jq -r 'map(select(test("-rc\\d+$"))) | max') + + if [[ $LAST_RC_VERSION == "$VERSION_NPM_FORMAT"* ]]; then + NEXT_RC_VERSION=$(echo "$LAST_RC_VERSION" | awk -F '-rc' '{print $1 "-rc" $2 + 1}') + RC_SUFFIX=$(echo "$NEXT_RC_VERSION" | sed -n 's/.*-rc\([0-9]*\)/-rc\1/p') + else + RC_SUFFIX="-rc1" + fi; + + NPM_PACKAGE_VERSION=${MVN_BASE_VERSION}${RC_SUFFIX} + else + echo "::debug::Release version found." + NPM_PACKAGE_VERSION_TAG="latest" + NPM_PACKAGE_VERSION=${MVN_PACKAGE_VERSION} + fi; + echo "::debug::NPM_PACKAGE_VERSION: $NPM_PACKAGE_VERSION" + echo "::debug::NPM_PACKAGE_VERSION_TAG: $NPM_PACKAGE_VERSION_TAG" + + echo "npm-package-version=$NPM_PACKAGE_VERSION" >> $GITHUB_OUTPUT + echo "npm-package-version-tag=$NPM_PACKAGE_VERSION_TAG" >> $GITHUB_OUTPUT + echo "::endgroup::" + shell: bash + + + # Sets up the NPM package + # Creates the bin folder with the binaries + # Adds the postinstall.js script + # Generates the package.json file with Jinja2 + + - name: 'NPM Package setup' + working-directory: ${{ github.workspace }}/tools/dotcms-cli/npm/ + env: + NPM_PACKAGE_NAME: ${{ inputs.npm-package-name }} + NPM_PACKAGE_VERSION: ${{ steps.npm-version-tag.outputs.npm-package-version }} + MVN_PACKAGE_NAME: dotcms-cli + MVN_PACKAGE_VERSION: ${{ steps.project.outputs.version }} + run: | + echo "::group::NPM Package setup" + echo "Adding bin folder with all the binaries" + mkdir -p bin + find ${{ github.workspace }}/artifacts/cli/target/distributions/ -name "*.zip" -exec unzip -d bin {} \; + + echo "Adding wrapper script" + mv src/postinstall.js.seed src/postinstall.js + + echo "Adding README.md file" + cp ${{ github.workspace }}/tools/dotcms-cli/README.md . + + echo "Adding package.json file" + jinja2 package.j2 -D packageName=${MVN_PACKAGE_NAME} -D npmPackageName=${NPM_PACKAGE_NAME} -D npmPackageVersion=${NPM_PACKAGE_VERSION} -D packageVersion=${MVN_PACKAGE_VERSION} --format json -o package.json + rm -f package.j2 + + cat package.json + cat src/postinstall.js + echo "::endgroup::" + shell: bash + + - name: 'NPM Package tree' + run: ls -R ${{ github.workspace }}/tools/dotcms-cli/npm/ + shell: bash + + - name: 'Validate NPM package' + working-directory: ${{ github.workspace }}/tools/dotcms-cli/npm + run: | + echo "::group::NPM package contents" + if [ ! -f package.json ]; then + echo "::error::NPM package not found. Exiting..." + exit 1 + else + echo "::notice::NPM package found. Proceeding..." + cat package.json + fi + echo "::endgroup::" + shell: bash + + - name: 'Publish to NPM registry' + working-directory: ${{ github.workspace }}/tools/dotcms-cli/npm + env: + NPM_AUTH_TOKEN: ${{ inputs.npm-token }} + NPM_PACKAGE_VERSION_TAG: ${{ steps.npm-version-tag.outputs.npm-package-version-tag }} + run: | + echo "//registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN}" > ~/.npmrc + npm publish --access public --tag ${NPM_PACKAGE_VERSION_TAG} + shell: bash diff --git a/.github/workflows/build-test-nightly.yml b/.github/workflows/build-test-nightly.yml index 598a272843e1..bbbe789e4e52 100644 --- a/.github/workflows/build-test-nightly.yml +++ b/.github/workflows/build-test-nightly.yml @@ -12,6 +12,10 @@ on: type: boolean description: 'Indicates if the workflow should build on missing artifacts' default: true + publish-npm-package: + type: boolean + description: 'Indicates if the workflow should publish the NPM package on the registry' + default: false run-all-tests: description: 'Run all tests' type: boolean @@ -60,12 +64,15 @@ jobs: artifact-run-id: ${{ needs.initialize.outputs.artifact-run-id }} environment: nightly deploy-dev-image: true + publish-npm-package: ${{ ( github.event_name == 'workflow_dispatch' && inputs.publish-npm-package == true ) || github.event_name == 'schedule' }} + reuse-previous-build: ${{ inputs.reuse-previous-build || false }} secrets: DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }} EE_REPO_USERNAME: ${{ secrets.EE_REPO_USERNAME }} EE_REPO_PASSWORD: ${{ secrets.EE_REPO_PASSWORD }} DEVELOPERS_SLACK_WEBHOOK: ${{ secrets.DEVELOPERS_SLACK_WEBHOOK }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} finalize: name: Finalize if: always() diff --git a/.github/workflows/publish-npm-package.yml b/.github/workflows/publish-npm-package.yml deleted file mode 100644 index 9d48ba6ae0d4..000000000000 --- a/.github/workflows/publish-npm-package.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: 'Publish NPM Package' -on: - workflow_call: - inputs: - ref: - description: 'Ref to checkout' - required: true - type: string - artifact-id: - description: 'Artifact id' - required: true - type: string - run-id: - description: 'Run id' - required: true - type: number - npm-package-version-tag: - description: 'NPM package version tag' - required: true - type: string - secrets: - gh-pat: - description: 'GitHub Personal Access Token' - required: true - npm-token: - description: 'NPM registry token' - required: false - -env: - NODE_VERSION: 19 - -jobs: - publish-npm-package: - runs-on: ubuntu-latest - steps: - - name: 'Checkout' - uses: actions/checkout@v4 - with: - ref: ${{ inputs.ref }} - - - uses: ./.github/actions/cleanup-runner - - - name: 'Set up Node.js' - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: 'Download all build artifacts' - uses: actions/download-artifact@v4 - with: - path: ${{ github.workspace }}/npm-package - name: ${{ inputs.artifact-id }} - run-id: ${{ inputs.run-id }} - github-token: ${{ secrets.gh-pat }} # token with actions:read permissions on target repo - - - name: 'Validate NPM package' - run: | - echo "::group::NPM package contents" - if [ ! -f ${{ github.workspace }}/npm-package/package.json ]; then - echo "::error::NPM package not found. Exiting..." - exit 1 - else - echo "::notice::NPM package found. Proceeding..." - cat ${{ github.workspace }}/npm-package/package.json - fi - - - name: 'Publish to NPM registry' - if: success() - working-directory: ${{ github.workspace }}/npm-package - env: - NPM_AUTH_TOKEN: ${{ secrets.npm-token }} - NPM_PACKAGE_VERSION_TAG: ${{ inputs.npm-package-version-tag }} - run: | - echo "//registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN}" > ~/.npmrc - npm publish --access public --tag ${NPM_PACKAGE_VERSION_TAG} diff --git a/.github/workflows/reusable-deployment.yml b/.github/workflows/reusable-deployment.yml index 499b6c8cb7d6..28f654c3cf76 100644 --- a/.github/workflows/reusable-deployment.yml +++ b/.github/workflows/reusable-deployment.yml @@ -15,6 +15,12 @@ on: deploy-dev-image: default: false type: boolean + reuse-previous-build: + default: false + type: boolean + publish-npm-package: + default: false + type: boolean secrets: DOCKER_USERNAME: required: false @@ -31,6 +37,9 @@ on: DEVELOPERS_SLACK_WEBHOOK: required: false description: 'Slack webhook for developers' + NPM_TOKEN: + required: false + description: 'NPM token' jobs: deployment: runs-on: ubuntu-latest @@ -86,6 +95,14 @@ jobs: build-run-id: ${{ github.run_id }} # We are currently building these artifacts in the current run commit-id: ${{ github.sha }} + - name: CLI Publish + if: inputs.publish-npm-package + uses: ./.github/actions/publish-npm-cli + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + npm-token: ${{ secrets.NPM_TOKEN }} + reuse-previous-build: ${{ inputs.reuse-previous-build }} + # A Slack notification is sent using the 'action-slack-notify' action if the repository is 'dotcms/core'. - name: Slack Notification if: github.repository == 'dotcms/core' @@ -98,4 +115,4 @@ jobs: MSG_MINIMAL: true SLACK_FOOTER: "" SLACK_ICON: https://avatars.githubusercontent.com/u/1005263?s=200&v=4 - SLACK_MESSAGE: "This automated script is happy to announce that a new docker image has been built for *master* with tags: [${{ steps.docker_build.outputs.tags }}] :docker:" \ No newline at end of file + SLACK_MESSAGE: "This automated script is happy to announce that a new docker image has been built for *${{ inputs.environment }}* with tags: [${{ steps.docker_build.outputs.tags }}] :docker:"