diff --git a/.github/actions/azdo-build/action.yml b/.github/actions/azdo-build/action.yml new file mode 100644 index 0000000000..27dfedf95d --- /dev/null +++ b/.github/actions/azdo-build/action.yml @@ -0,0 +1,425 @@ +name: 'Build via AzDO' +description: Run a build pipeline via Azure DevOps and download the build artifact + +inputs: + artifact-name: + description: Output artifact name + default: azdo-artifact + type: string + + azdo-org: + description: AzDO org URL + required: true + type: string + + azdo-pipeline-name: + description: AzDO pipeline name + required: true + type: string + + azdo-project: + description: AzDO project name + required: true + type: string + + azure-client-id: + description: Azure client ID + required: true + type: string + + azure-subscription-id: + description: Azure subscription ID + required: true + type: string + + azure-tenant-id: + description: Azure tenant ID + required: true + type: string + + branch-name: + description: Branch name + default: main + type: string + + download-artifact-name: + description: Name of AzDO artifact to download + required: true + type: string + + pipeline-variable: + description: Pipeline variable + required: true + type: string + + # dist-tag: + # description: Dist-tag + # default: main + # type: string + + # version: + # description: Version + # required: true + # type: string + +outputs: + version: + description: Version + value: ${{ steps.get-version.outputs.version }} + + version-type: + description: Version + value: ${{ steps.get-version.outputs.version-type }} + +runs: + using: composite + steps: + - name: Install Azure CLI + run: | + if ! command -v az > /dev/null; then + curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash + fi + + if ! command -v jq > /dev/null; then + sudo apt-get update + # sudo apt-get install -y icu jq + sudo apt-get install -y jq + fi + shell: bash + + # - name: Azure login + # run: | + # # az login \ + # # --client-id ${{ inputs.azure-client-id }} \ + # # --federated-token ${{ github.token }} \ + # # --identity \ + # # --subscription ${{ inputs.azure-subscription-id }} \ + # # --tenant ${{ inputs.azure-tenant-id }} + + # az login \ + # --allow-no-subscription \ + # --federated-token ${{ github.token }} \ + # --service-principal \ + # --tenant ${{ inputs.azure-tenant-id }} \ + # --username ${{ inputs.azure-client-id }} + # shell: bash + + - uses: azure/login@v2 + with: + allow-no-subscriptions: true + client-id: ${{ inputs.azure-client-id }} + tenant-id: ${{ inputs.azure-tenant-id }} + + - id: run-pipeline + run: | + set -e -o pipefail + + az extension add --name azure-devops --only-show-errors + # az pipelines list --org ${{ inputs.azdo-org }} --project ${{ inputs.azdo-project }} + OUTPUT=$( + az pipelines run \ + --detect false --output json) \ + --name ${{ inputs.azdo-pipeline-name }} \ + --org ${{ inputs.azdo-org }} \ + --project ${{ inputs.azdo-project }} \ + --variable ${{ inputs.pipeline-variable }} + ) + + RUN_ID=$(echo "$OUTPUT" | jq -r '.id') + RUN_URL=$(echo "$OUTPUT" | jq -r '.url') + + if [ -z "$RUN_ID" ] || [ "$RUN_ID" = "null" ]; then + echo "$OUTPUT" + echo "Failed to extract run ID from output" + exit 1 + fi + + if [ -z "$RUN_URL" ] || [ "$RUN_URL" = "null" ]; then + echo "$OUTPUT" + echo "Failed to extract run URL from output" + exit 1 + fi + + echo "run-id=$RUN_ID" | tee --append $GITHUB_OUTPUT + echo "url=$RUN_URL" | tee --append $GITHUB_OUTPUT + shell: bash + + # Temporarily not running pipeline but getting from existing result + # - id: run-pipeline + # name: Run pipeline + # run: | + # echo run-id=424951 | tee --append $GITHUB_OUTPUT + # echo url=https://fuselabs.visualstudio.com/531382a8-71ae-46c8-99eb-9512ccb91a43/_apis/build/Builds/424951 | tee --append $GITHUB_OUTPUT + # shell: bash + + - name: Wait for pipeline completion + run: | + set -e -o pipefail + + az extension add --name azure-devops --only-show-errors + + RUN_ID="${{ steps.run-pipeline.outputs.run-id }}" + echo "Waiting for run ID: $RUN_ID to complete..." + + # Timeout after 45 minutes (2700 seconds) + TIMEOUT=2700 + ELAPSED=0 + INTERVAL=5 + + while [ $ELAPSED -lt $TIMEOUT ]; do + OUTPUT=$( + az pipelines runs show \ + --id "$RUN_ID" \ + --org ${{ inputs.azdo-org }} \ + --project ${{ inputs.azdo-project }} \ + --detect false \ + --output json + ) + STATUS=$(echo "$OUTPUT" | jq -r '.status') + + if [ -z "$STATUS" ] || [ "$STATUS" = "null" ]; then + echo "Failed to extract status from output" + exit 1 + fi + + echo "Current status: $STATUS (elapsed: ${ELAPSED}s)" + + # Check for terminal states + if [ "$STATUS" = "completed" ]; then + echo "Pipeline completed!" + RESULT=$(echo "$OUTPUT" | jq -r '.result') + echo "Result: $RESULT" + + if [ "$RESULT" != "succeeded" ]; then + echo "Pipeline failed with result: $RESULT" + exit 1 + fi + + echo "Pipeline succeeded!" + exit 0 + elif [ "$STATUS" = "canceling" ] || [ "$STATUS" = "canceled" ]; then + echo "Pipeline was canceled" + exit 1 + fi + + sleep $INTERVAL + ELAPSED=$((ELAPSED + INTERVAL)) + done + + echo "Timeout reached after ${TIMEOUT} seconds for run ID: $RUN_ID" + exit 1 + shell: bash + + - name: Download artifact from Azure DevOps + run: | + set -e -o pipefail + + az extension add --name azure-devops --only-show-errors + + RUN_ID="${{ steps.run-pipeline.outputs.run-id }}" + echo "Downloading artifact 'drop_build_main' from run ID: $RUN_ID" + + # Create directory for artifact + mkdir -p ./artifact-download/ + + # Download the artifact + az pipelines runs artifact download \ + --artifact-name "drop_build_main" \ + --detect false \ + --org ${{ inputs.azdo-org }} \ + --path "$GITHUB_WORKSPACE/artifact-download/" \ + --project ${{ inputs.azdo-project }} \ + --run-id "$RUN_ID" + + # Verify artifact was downloaded + if [ -z "$(ls -A artifact-download)" ]; then + echo "Artifact directory is empty after download" + exit 1 + fi + + echo "Artifact downloaded successfully" + shell: bash + + - name: Upload artifact + uses: actions/upload-artifact@v6 + with: + name: ${{ inputs.artifact-name }} + path: ./artifact-download/ + + - id: get-version + name: Get version + run: | + VERSION=$(tar -xzOf botframework-webchat-core-*.tgz package/package.json | jq -r '.version') + + echo version=$VERSION | tee --append $GITHUB_OUTPUT + + if [[ "$VERSION" == *-0 ]]; then echo version-type=prerelease | tee --append $GITHUB_OUTPUT; else echo version-type=production | tee --append $GITHUB_OUTPUT; fi + shell: bash + working-directory: ./artifact-download/tgzfiles + +# prepare-release: +# name: Prepare release +# needs: +# - upload-pipeline-artifact +# outputs: +# prerelease: ${{ steps.save-version.outputs.prerelease }} +# version: ${{ steps.save-version.outputs.version }} +# permissions: +# actions: read +# contents: read +# id-token: write +# runs-on: ubuntu-latest + +# steps: +# - uses: actions/checkout@v6 +# - uses: actions/setup-node@v6 + +# - name: Download build artifact +# uses: actions/download-artifact@v7 +# with: +# name: drop_build_main +# path: ./azdo-artifact/ + +# - name: Prepare CDN artifact +# run: | +# mkdir -p ./github-artifact/cdn/ +# cd ./github-artifact/cdn/ +# unzip ../../azdo-artifact/cdn_files/CdnFilesUpload.zip + +# - name: Upload CDN artifact +# uses: actions/upload-artifact@v6 +# with: +# name: asset-cdn +# path: ./github-artifact/cdn/ + +# - name: Prepare tarball artifact +# run: | +# mkdir -p ./github-artifact/tarball/ +# cd ./github-artifact/tarball/ +# cp ../../azdo-artifact/tgzfiles/* . + +# - name: Upload tarball artifact +# uses: actions/upload-artifact@v6 +# with: +# name: asset-tarball +# path: ./github-artifact/tarball/ + +# - id: save-version +# name: Save version +# run: | +# version=$(cat version.txt) + +# prerelease=$([[ "$version" == *-* ]] && echo true || echo false) + +# echo prerelease="$prerelease" | tee --append $GITHUB_OUTPUT +# echo version="$version" | tee --append $GITHUB_OUTPUT +# working-directory: ./azdo-artifact/version/ + +# - name: Prepare release notes +# run: | +# cp ./.github/release_notes_template.md ./release_notes.md + +# node \ +# --eval "import fs from 'node:fs'; fs.writeFileSync(process.argv[1], fs.readFileSync(process.argv[1], 'utf8').replaceAll(process.argv[2], process.argv[3]));" \ +# --input-type=module \ +# -- \ +# ./release_notes.md \ +# {{webchat-js-integrity}} \ +# "sha384-$(openssl dgst -sha384 -binary ./github-artifact/cdn/webchat.js | openssl base64 -A)" + +# node \ +# --eval "import fs from 'node:fs'; fs.writeFileSync(process.argv[1], fs.readFileSync(process.argv[1], 'utf8').replaceAll(process.argv[2], process.argv[3]));" \ +# --input-type=module \ +# -- \ +# ./release_notes.md \ +# {{webchat-minimal-js-integrity}} \ +# "sha384-$(openssl dgst -sha384 -binary ./github-artifact/cdn/webchat-minimal.js | openssl base64 -A)" + +# node \ +# --eval "import fs from 'node:fs'; fs.writeFileSync(process.argv[1], fs.readFileSync(process.argv[1], 'utf8').replaceAll(process.argv[2], process.argv[3]));" \ +# --input-type=module \ +# -- \ +# ./release_notes.md \ +# {{version}} \ +# "${{ steps.save-version.outputs.version }}" + +# cat ./release_notes.md + +# - name: Upload release notes +# uses: actions/upload-artifact@v6 +# with: +# name: release-notes +# path: ./release_notes.md + +# create-release: +# name: Create release +# needs: +# - prepare-release +# outputs: +# release-tag: ${{ steps.create-release.outputs.release-tag }} +# permissions: +# contents: write +# runs-on: ubuntu-latest + +# steps: +# - name: Download release notes +# uses: actions/download-artifact@v7 +# with: +# name: release-notes +# path: release-notes + +# - env: +# GH_TOKEN: ${{ github.token }} +# id: create-release +# name: Create release +# run: | +# if [[ "${{ needs.prepare-release.outputs.prerelease }}" == "true" ]]; then prerelease=1; fi + +# release_tag=v${{ needs.prepare-release.outputs.version }} + +# gh release create $release_tag \ +# --draft \ +# --notes-file ./release-notes/release_notes.md \ +# ${prerelease:+--prerelease} \ +# --repo ${{ github.repository }} \ +# --target ${{ github.ref }} + +# echo release-tag=$release_tag | tee --append $GITHUB_OUTPUT + +# upload-release-asset: +# name: Upload release asset +# needs: +# - create-release +# permissions: +# contents: write +# runs-on: ubuntu-latest + +# steps: +# - name: Download asset artifact +# uses: actions/download-artifact@v7 +# with: +# merge-multiple: true +# path: ./asset/ +# pattern: asset-* + +# - env: +# GH_TOKEN: ${{ github.token }} +# name: Upload assets +# run: | +# gh release upload ${{ needs.create-release.outputs.release-tag }} \ +# ./asset/* \ +# --repo ${{ github.repository }} + +# - env: +# GH_TOKEN: ${{ github.token }} +# name: Publish release +# run: | +# gh release edit ${{ needs.create-release.outputs.release-tag }} \ +# --draft=false \ +# --repo ${{ github.repository }} + + # - continue-on-error: true + # if: ${{ always() }} + # name: Azure logout + # run: az logout + # shell: bash diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml new file mode 100644 index 0000000000..3cf59bb299 --- /dev/null +++ b/.github/workflows/create-release.yml @@ -0,0 +1,374 @@ +name: 🚀 Create release + +on: + push: # Temporarily added for testing + branches: feat-create-request + workflow_dispatch: + inputs: + skip-release: + default: false + description: Skip release + type: boolean + +defaults: + run: + shell: bash + +env: + node-version: 22 + +jobs: + # prepare: + # name: Prepare + # outputs: + # branch-name: ${{ steps.get-branch-name.outputs.branch-name }} + # permissions: + # contents: read + # runs-on: ubuntu-latest + + # steps: + # - uses: actions/checkout@v6 + + # - id: get-branch-name + # name: Get branch name + # run: | + # BRANCH_NAME=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --short HEAD) + + # echo branch-name=$BRANCH_NAME | tee --append $GITHUB_OUTPUT + + # # - uses: actions/setup-node@v6 + # # with: + # # node-version: ${{ env.node-version }} + + # # - id: version-type + # # name: Determine version type + # # run: 'if [[ "$(cat ./package.json | jq -r ''.version'')" == *-0 ]]; then echo version-type=prerelease | tee --append $GITHUB_OUTPUT; else echo version-type=production | tee --append $GITHUB_OUTPUT; fi' + + # # - if: steps.version-type.outputs.version-type != 'production' + # # name: Set version + # # run: | + # # BRANCH_NAME=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --short HEAD) + # # COMMITISH=${{ github.sha }} + # # NOW=$(date +%Y%m%d%H%M) + + # # SHORT_COMMITISH=${COMMITISH:0:7} + + # # # npm version simply ignoring the build metadata + (plus) sign, we need to use dot or hyphen instead. + # # npm version --no-git-tag-version $(echo $(cat ./package.json | jq -r '.version') | cut -d- -f1)-$BRANCH_NAME.$NOW.$SHORT_COMMITISH + + # # - id: version + # # name: Get version + # # run: echo version=$(cat package.json | jq -r '.version') | tee --append $GITHUB_OUTPUT + + # build: + # name: Build + # permissions: + # contents: read + # needs: prepare + # runs-on: ubuntu-latest + + # steps: + # - uses: actions/checkout@v6 + + # - uses: actions/setup-node@v6 + # with: + # node-version: ${{ env.node-version }} + + # - run: npm clean-install + + # - name: npm version ${{ needs.prepare.outputs.version }} + # run: | + # npm version ${{ needs.prepare.outputs.version }} \ + # --include-workspace-root \ + # --no-git-tag-version \ + # --no-workspaces-update \ + # --workspaces + + # - env: + # NODE_ENV: production + # run: npm run build + + # - name: Pack as tarball + # run: | + # npm pack \ + # --workspace=./packages/core \ + # --workspace=./packages/api \ + # --workspace=./packages/component \ + # --workspace=./packages/bundle \ + # --workspace=./packages/fluent-theme + + # # - name: Generate SBOM + # # # --workspace has no effect, the resulting SBOM still contains other packages in the workspace + # # run: npm sbom --package-lock-only --sbom-format spdx --sbom-type library | tee ./sbom.spdx.json + + # - uses: actions/upload-artifact@v7 + # with: + # name: tarball + # path: ./*.tgz + + # - uses: actions/upload-artifact@v7 + # with: + # name: bundle-iife + # path: ./packages/bundle/dist/webchat*.js + + # - uses: actions/upload-artifact@v7 + # with: + # name: bundle-esm + # path: ./packages/bundle/static/**/* + + # - uses: actions/upload-artifact@v7 + # with: + # name: fluent-theme-iife + # path: ./packages/fluent-theme/dist/**/* + + # - uses: actions/upload-artifact@v7 + # with: + # name: fluent-theme-esm + # path: ./packages/fluent-theme/static/**/* + + # # - name: Upload SBOM artifact + # # uses: actions/upload-artifact@v7 + # # with: + # # name: sbom + # # path: ./sbom.spdx.json + + build: + environment: azure-devops + name: Build + outputs: + version: ${{ steps.azdo-build.outputs.version }} + version-type: ${{ steps.azdo-build.outputs.version-type }} + permissions: + contents: read + id-token: write + runs-on: ubuntu-slim + + steps: + - uses: actions/checkout@v6 + + - id: get-branch-name + name: Get branch name + run: | + BRANCH_NAME=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --short HEAD) + + echo branch-name=$BRANCH_NAME | tee --append $GITHUB_OUTPUT + + - id: azdo-build + uses: ./.github/actions/azdo-build + with: + artifact-name: azdo-artifact + azdo-org: ${{ vars.AZDO_ORG }} + azdo-pipeline-name: ${{ vars.AZDO_PIPELINE_NAME }} + azdo-project: ${{ vars.AZDO_PROJECT }} + azure-client-id: ${{ vars.AZURE_CLIENT_ID }} + azure-subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} + azure-tenant-id: ${{ vars.AZURE_TENANT_ID }} + branch-name: ${{ steps.get-branch-name.outputs.branch-name }} + download-artifact-name: drop_build_main + pipeline-variable: ProjectBranch=${{ inputs.branch-name }} + + - name: Download AzDO build artifact + uses: actions/download-artifact@v8 + with: + name: azdo-artifact + + - name: Extract artifact (bundle) + run: | + mkdir -p ./bundle/ + cd ./bundle/ + + tar \ + --extract \ + --file=../tgzfiles/botframework-webchat-${{ steps.azdo-build.outputs.version }}.tgz \ + --strip-component=1 \ + package/dist/ \ + package/static/ + + - name: Extract artifact (fluent-theme) + run: | + mkdir -p ./fluent-theme/ + cd ./fluent-theme/ + + tar \ + --extract \ + --file=../tgzfiles/botframework-webchat-fluent-theme-${{ steps.azdo-build.outputs.version }}.tgz \ + --strip-component=1 \ + package/dist/ \ + package/static/ + + - name: Upload artifact (tarball) + uses: actions/upload-artifact@v7 + with: + name: tarball + path: ./tgzfiles/*.tgz + + - name: Upload artifact (bundle-iife) + uses: actions/upload-artifact@v7 + with: + name: bundle-iife + path: ./bundle/dist/webchat*.js + + - name: Upload artifact (bundle-esm) + uses: actions/upload-artifact@v7 + with: + name: bundle-esm + path: ./bundle/static/ + + - name: Upload artifact (fluent-theme-iife) + uses: actions/upload-artifact@v7 + with: + name: fluent-theme-iife + path: ./fluent-theme/dist/webchat*.js + + - name: Upload artifact (fluent-theme-esm) + uses: actions/upload-artifact@v7 + with: + name: fluent-theme-esm + path: ./fluent-theme/static/ + + - name: Upload artifact (sbom) + uses: actions/upload-artifact@v7 + with: + name: sbom + path: ./_manifest/spdx_2.2/manifest.spdx.json + + upload-changelog: + name: Upload changelog + needs: build + permissions: + contents: read + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: ${{ env.node-version }} + + - run: npm install --global keep-a-changelog@2 prettier + + - if: needs.build.outputs.version-type != 'production' + name: Tag unreleased as latest + run: npx keep-a-changelog --format markdownlint --release=${{ needs.build.outputs.version }} + + - name: Extract latest entry + run: npx keep-a-changelog --format markdownlint --latest-release-full | tee ./CHANGELOG.latest.md + + - name: Format extracted entry + run: npx prettier CHANGELOG.latest.md --tab-width 3 --write + + - name: Upload changelog + uses: actions/upload-artifact@v7 + with: + name: changelog + path: ./CHANGELOG.latest.md + + release: + environment: + name: github-release + url: ${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ steps.release.outputs.tag }} + name: Release + needs: + - build + - upload-changelog + permissions: + contents: write + runs-on: ubuntu-latest + + steps: + - name: Download artifact (tarball) + uses: actions/download-artifact@v8 + with: + name: tarball + path: ./asset + + - name: Download artifact (bundle-iife) + uses: actions/download-artifact@v8 + with: + name: bundle-iife + path: ./asset + + - name: Download artifact (changelog) + uses: actions/download-artifact@v8 + with: + name: changelog + path: ./ + + - name: Download artifact (sbom) + uses: actions/download-artifact@v8 + with: + name: sbom + path: ./asset + + - id: compute-hash + name: Compute build metadata + run: | + echo git-short-sha=`echo ${{ github.sha }} | cut -c 1-7` | tee --append $GITHUB_OUTPUT + echo release-date=`date "+%Y-%m-%d %R:%S"` | tee --append $GITHUB_OUTPUT + echo sha384-es5=`cat webchat-es5.js | openssl dgst -sha384 -binary | openssl base64 -A` | tee --append $GITHUB_OUTPUT + echo sha384-full=`cat webchat.js | openssl dgst -sha384 -binary | openssl base64 -A` | tee --append $GITHUB_OUTPUT + echo sha384-minimal=`cat webchat-minimal.js | openssl dgst -sha384 -binary | openssl base64 -A` | tee --append $GITHUB_OUTPUT + + - name: Build release notes + run: | + tee ./release.txt < + + + + + \`\`\` + + # Changelog + + EOF + + cat ./CHANGELOG.latest.md | tee --append ./release.txt + + - env: + # Use actions/create-github-app-token if create release would need to trigger another workflow. + GH_TOKEN: ${{ github.token }} + id: release + name: Create release + # Do not upload assets while creating release, otherwise, it will not trigger "release created" event. + run: | + if [[ "${{ needs.build.outputs.version-type }}" == "prerelease" ]]; then PRERELEASE=1; fi + + TAG=v${{ needs.build.outputs.version }} + + gh release create $TAG \ + --notes-file ./release.txt \ + ${PRERELEASE:+--prerelease} \ + --repo ${{ github.repository }} \ + --target ${{ github.ref }} + + echo tag=$TAG | tee --append $GITHUB_OUTPUT + - env: + GH_TOKEN: ${{ github.token }} + name: Upload assets + run: | + gh release upload ${{ steps.release.outputs.tag }} \ + --repo ${{ github.repository }} \ + ./asset/*.js \ + ./asset/*.tgz \ + ./asset/manifest.spdx.json