Upload dev build from PR #401
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Upload dev build from PR | |
| # See https://github.com/plotly/plotly.js/blob/master/CONTRIBUTING.md#live-links-to-dev-builds | |
| # for documentation on the usage of this workflow. | |
| on: | |
| workflow_run: | |
| workflows: ["Publish Dist"] # publish-dist.yml | |
| types: | |
| - completed | |
| workflow_dispatch: | |
| inputs: | |
| pr_number: | |
| description: 'PR Number to deploy' | |
| required: false | |
| env: | |
| ARTIFACT_UPLOAD_WORKFLOW_NAME: "Publish Dist" | |
| UPLOAD_DIR_NAME: "upload" | |
| jobs: | |
| upload: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| checks: write | |
| # Only run on manual dispatch, | |
| # OR if the parent run succeeded and was triggered by a PR. | |
| # Fork PRs run through so we can post a "skipped" commit status to the PR | |
| # head and log why; the actual upload steps are guarded by fork-check so no | |
| # dev build is published for fork PRs (they lack DEV_DEPLOY_APP access). | |
| if: | | |
| github.event_name == 'workflow_dispatch' || | |
| ( | |
| github.event_name == 'workflow_run' && | |
| github.event.workflow_run.event == 'pull_request' && | |
| github.event.workflow_run.conclusion == 'success' | |
| ) | |
| steps: | |
| - name: Check if PR is from a fork | |
| id: fork-check | |
| env: | |
| HEAD_REPO: ${{ github.event.workflow_run.head_repository.full_name }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| if [ -n "${HEAD_REPO}" ] && [ "${HEAD_REPO}" != "${REPO}" ]; then | |
| echo "::notice::Dev-build upload skipped: PR is from a fork (${HEAD_REPO}). Dev builds are only uploaded for PRs from branches within ${REPO}. A maintainer can trigger a build manually via workflow_dispatch with the PR number as input." | |
| echo "is_fork=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "is_fork=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Get required metadata (PR number, commit SHA, workflow run ID containing artifacts) | |
| id: get-metadata | |
| if: steps.fork-check.outputs.is_fork != 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_EVENT_NAME: ${{ github.event_name }} | |
| GH_REPO: ${{ github.repository }} | |
| SHA: ${{ github.event.workflow_run.head_sha }} | |
| RUN_ID: ${{ github.event.workflow_run.id }} | |
| PR_NUM: ${{ github.event.workflow_run.pull_requests[0].number || inputs.pr_number }} | |
| run: | | |
| # Get SHA from manually-provided PR number if triggered by workflow_dispatch | |
| if [ "${GH_EVENT_NAME}" == "workflow_dispatch" ]; then | |
| if [ -n "${PR_NUM}" ]; then | |
| SHA=$(gh pr view "${PR_NUM}" --repo "${GH_REPO}" --json headRefOid --template '{{.headRefOid}}') | |
| fi | |
| fi | |
| # At this point, SHA should be defined. If not, fail the workflow | |
| if [ -z "${SHA}" ]; then | |
| echo "Failed to get commit SHA, exiting" | |
| exit 1 | |
| fi | |
| # If PR_NUM is empty, get PR number using SHA | |
| if [ -z "${PR_NUM}" ]; then | |
| PR_NUM=$(gh pr list --search "sha:${SHA}" --state open --json number --jq '.[0].number') | |
| fi | |
| # Validate that we have a valid PR number | |
| if [ -z "${PR_NUM}" ] || [[ ! "${PR_NUM}" =~ ^[1-9][0-9]{0,4}$ ]]; then | |
| echo "Failed to get PR number, exiting (PR_NUM=${PR_NUM})" | |
| exit 1 | |
| fi | |
| # If RUN_ID is empty, use the gh CLI to get the most recent run ID for SHA | |
| if [ -z "${RUN_ID}" ]; then | |
| RUN_ID=$(gh run list \ | |
| --workflow "${ARTIFACT_UPLOAD_WORKFLOW_NAME}" \ | |
| --commit "${SHA}" \ | |
| --limit 1 \ | |
| --json databaseId \ | |
| --jq '.[0].databaseId') | |
| fi | |
| # At this point, RUN_ID should be defined. If not, fail the workflow | |
| if [ -z "${RUN_ID}" ]; then | |
| echo "Failed to get workflow run ID, exiting" | |
| exit 1 | |
| fi | |
| # Save PR number, commit SHA, short SHA, and run ID to output | |
| echo "PR_NUM=${PR_NUM}" >> $GITHUB_OUTPUT | |
| echo "SHA=${SHA}" >> $GITHUB_OUTPUT | |
| echo "SHORT_SHA=${SHA:0:7}" >> $GITHUB_OUTPUT | |
| echo "RUN_ID=${RUN_ID}" >> $GITHUB_OUTPUT | |
| - name: Download build artifact | |
| id: download-artifact | |
| if: steps.fork-check.outputs.is_fork != 'true' | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: dist # uploaded by publish-dist.yml > publish-dist | |
| run-id: ${{ steps.get-metadata.outputs.RUN_ID }} | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| path: temp-dist | |
| - name: Prepare folders | |
| id: setup-metadata | |
| if: steps.fork-check.outputs.is_fork != 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PR_NUM: ${{ steps.get-metadata.outputs.PR_NUM }} | |
| SHA: ${{ steps.get-metadata.outputs.SHA }} | |
| SHORT_SHA: ${{ steps.get-metadata.outputs.SHORT_SHA }} | |
| run: | | |
| echo "SHA: ${SHA}" | |
| echo "Short SHA: ${SHORT_SHA}" | |
| echo "PR number: ${PR_NUM}" | |
| mkdir -p "${UPLOAD_DIR_NAME}/pr-${PR_NUM}/latest" | |
| mkdir -p "${UPLOAD_DIR_NAME}/pr-${PR_NUM}/${SHORT_SHA}" | |
| # Copy all 3 artifacts (plotly.js, plotly.min.js, plot-schema.json) to /latest/ | |
| cp temp-dist/plotly.js "${UPLOAD_DIR_NAME}/pr-${PR_NUM}/latest/plotly.js" | |
| cp temp-dist/plotly.min.js "${UPLOAD_DIR_NAME}/pr-${PR_NUM}/latest/plotly.min.js" | |
| cp temp-dist/plot-schema.json "${UPLOAD_DIR_NAME}/pr-${PR_NUM}/latest/plot-schema.json" | |
| # Copy only plotly.min.js to /$SHORT_SHA/ | |
| cp temp-dist/plotly.min.js "${UPLOAD_DIR_NAME}/pr-${PR_NUM}/${SHORT_SHA}/plotly.min.js" | |
| UPLOAD_DIR_FULL_PATH=$(pwd)/${UPLOAD_DIR_NAME}/ | |
| echo "Created directory ${UPLOAD_DIR_FULL_PATH} with the following contents:" | |
| echo "$(ls -lR ${UPLOAD_DIR_FULL_PATH})" | |
| - name: Generate GitHub App token | |
| id: generate-token | |
| if: steps.fork-check.outputs.is_fork != 'true' | |
| uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 #v3.1.1 | |
| with: | |
| client-id: ${{ vars.DEV_DEPLOY_APP_ID }} | |
| private-key: ${{ secrets.DEV_DEPLOY_APP_PRIVATE_KEY }} | |
| owner: plotly | |
| repositories: plotly.js-dev-builds | |
| - name: Check out plotly.js-dev-builds repo | |
| if: steps.fork-check.outputs.is_fork != 'true' | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| with: | |
| repository: plotly/plotly.js-dev-builds | |
| token: ${{ steps.generate-token.outputs.token }} # token from previous step | |
| path: plotly.js-dev-builds | |
| - name: Commit and push files | |
| id: commit-and-push | |
| if: steps.fork-check.outputs.is_fork != 'true' | |
| env: | |
| PR_NUM: ${{ steps.get-metadata.outputs.PR_NUM }} | |
| SHORT_SHA: ${{ steps.get-metadata.outputs.SHORT_SHA }} | |
| run: | | |
| # Move 'pr-NNNN/' directory into upload directory inside repo and cd into repo root | |
| TARGET_DIR="${UPLOAD_DIR_NAME}/pr-${PR_NUM}" | |
| mkdir -p "plotly.js-dev-builds/${UPLOAD_DIR_NAME}" | |
| cp -r "${TARGET_DIR}" "plotly.js-dev-builds/${UPLOAD_DIR_NAME}" | |
| cd plotly.js-dev-builds | |
| # Configure git | |
| git config user.name "plotly.js-pr-upload" | |
| git config user.email "<>" | |
| # Add files | |
| git add "${TARGET_DIR}/" | |
| # Ensure that only files in upload/pr-NNNN/ are staged | |
| if git diff --name-only --cached | grep -qv "^${TARGET_DIR}/"; then | |
| echo "Error: Changes detected outside ${TARGET_DIR}/" | |
| exit 1 | |
| fi | |
| # Only commit if there are changes | |
| if git diff --staged --quiet; then | |
| echo "No changes to commit" | |
| else | |
| git commit -m "Deploy build for PR #${PR_NUM} (commit ${SHORT_SHA})" | |
| git push origin main | |
| fi | |
| - name: Generate summary | |
| if: steps.fork-check.outputs.is_fork != 'true' | |
| env: | |
| PR_NUM: ${{ steps.get-metadata.outputs.PR_NUM }} | |
| SHORT_SHA: ${{ steps.get-metadata.outputs.SHORT_SHA }} | |
| run: | | |
| BASE_URL="https://plotly.github.io/plotly.js-dev-builds/${UPLOAD_DIR_NAME}/pr-${PR_NUM}" | |
| echo "### PR Build Uploaded" >> $GITHUB_STEP_SUMMARY | |
| echo "Builds for PR #${PR_NUM} can be accessed at:" >> $GITHUB_STEP_SUMMARY | |
| echo "- Latest build for this PR: [${BASE_URL}/latest/plotly.min.js](${BASE_URL}/latest/plotly.min.js)" >> $GITHUB_STEP_SUMMARY | |
| echo "- Build for this commit: [${BASE_URL}/${SHORT_SHA}/plotly.min.js](${BASE_URL}/${SHORT_SHA}/plotly.min.js)" >> $GITHUB_STEP_SUMMARY | |
| echo "The above links should start working a minute or two after this job completes." >> $GITHUB_STEP_SUMMARY | |
| - name: Report dev-build outcome to PR head | |
| # Reach this step for any run that resolved a PR head SHA — both | |
| # workflow_run (auto-triggered by Publish Dist) and workflow_dispatch | |
| # (a maintainer manually dispatching a build). The step then either | |
| # creates a "dev build" check-run on the PR head, or PATCHes the | |
| # existing one so the row updates in place instead of stacking. | |
| if: | | |
| always() && ( | |
| (github.event_name == 'workflow_run' && github.event.workflow_run.head_sha != '') || | |
| (github.event_name == 'workflow_dispatch' && steps.get-metadata.outputs.SHA != '') | |
| ) | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| SHA: ${{ github.event.workflow_run.head_sha || steps.get-metadata.outputs.SHA }} | |
| HEAD_REPO: ${{ github.event.workflow_run.head_repository.full_name }} | |
| IS_FORK: ${{ steps.fork-check.outputs.is_fork }} | |
| PR_NUM: ${{ steps.get-metadata.outputs.PR_NUM }} | |
| UPLOAD_OUTCOME: ${{ steps.commit-and-push.outcome }} | |
| RUN_ID: ${{ github.run_id }} | |
| run: | | |
| RUN_URL="https://github.com/${REPO}/actions/runs/${RUN_ID}" | |
| if [ "${IS_FORK}" == "true" ]; then | |
| TITLE="Skipped: PR is from a fork" | |
| CONCLUSION="neutral" | |
| SUMMARY=$(cat <<EOF | |
| Dev builds are not automatically published for PRs from forks (\`${HEAD_REPO}\`) as a security policy. | |
| If you need a dev build for this PR, please **contact a maintainer** and ask them to trigger the *Upload dev build from PR* workflow manually via the Actions tab, providing this PR's number as input. | |
| EOF | |
| ) | |
| DETAILS_URL="${RUN_URL}" | |
| elif [ "${UPLOAD_OUTCOME}" == "success" ]; then | |
| TITLE="Dev build available" | |
| CONCLUSION="success" | |
| URL="https://plotly.github.io/plotly.js-dev-builds/${UPLOAD_DIR_NAME}/pr-${PR_NUM}/latest/plotly.min.js" | |
| SUMMARY="Latest dev build for PR #${PR_NUM}: [${URL}](${URL})" | |
| DETAILS_URL="${URL}" | |
| else | |
| TITLE="Dev build upload failed" | |
| CONCLUSION="failure" | |
| SUMMARY="Upload workflow failed. See [the run](${RUN_URL}) for details." | |
| DETAILS_URL="${RUN_URL}" | |
| fi | |
| PAYLOAD=$(jq -n \ | |
| --arg conclusion "${CONCLUSION}" \ | |
| --arg title "${TITLE}" \ | |
| --arg summary "${SUMMARY}" \ | |
| --arg details_url "${DETAILS_URL}" \ | |
| '{ | |
| status: "completed", | |
| conclusion: $conclusion, | |
| details_url: $details_url, | |
| output: {title: $title, summary: $summary} | |
| }') | |
| # Look for an existing "dev build" check-run on this SHA so we can | |
| # PATCH it in place — e.g. so an initial "Skipped: fork" row updates | |
| # to "Dev build available" when a maintainer later dispatches a build. | |
| # If multiple exist (shouldn't happen under normal use), PATCH the most | |
| # recent one identified by the highest check-run id. | |
| EXISTING_ID=$(gh api "repos/${REPO}/commits/${SHA}/check-runs" \ | |
| --jq '[.check_runs[] | select(.name == "dev build")] | sort_by(.id) | last | .id // empty') | |
| if [ -n "${EXISTING_ID}" ]; then | |
| echo "${PAYLOAD}" | gh api --method PATCH --input - "repos/${REPO}/check-runs/${EXISTING_ID}" | |
| else | |
| echo "${PAYLOAD}" | jq --arg sha "${SHA}" '. + {name: "dev build", head_sha: $sha}' \ | |
| | gh api --method POST --input - "repos/${REPO}/check-runs" | |
| fi |