/
vshmidt
/
streamlit
Обзор
Документация
Войти
/
vshmidt
/
streamlit
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
.github/workflows/pr-preview.yml
790 строк
30 KB
Lukas Masuch
Migrate to uv package manager (#13622)
30 янв 2026, 01:23
Не верифицирован
30 янв 2026, 01:23
4d386cc
Код
Авторство
О чём код?
name: PR Preview on: push: branches: - "develop" pull_request: types: [opened, synchronize, reopened] paths-ignore: # Don't run CI if changes are only in documentation/spec folders: - "specs/**" - "agent-knowledge/**" - "wiki/**" # Avoid duplicate workflows on same branch concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: initial-pr-comment: runs-on: ubuntu-latest # Only run on PRs that are not from forks if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository permissions: pull-requests: write steps: - name: Checkout repository uses: actions/checkout@v6 with: fetch-depth: 1 # Only create a comment if the AWS_ACCESS_KEY_ID is set # The API key is not set if this is from a fork PR or from dependabot. - if: ${{ env.AWS_ACCESS_KEY_ID != '' }} name: Create or update comment (Building) id: initial-comment uses: actions/github-script@v8 env: AWS_ACCESS_KEY_ID: ${{ secrets.CORE_PREVIEWS_S3_KEY_ID }} with: script: | // Define the building template const buildingTemplate = "### 🚧 PR preview is building...\n\n" + "| Name | Link |\n" + "| :----: | ---- |\n" + "| 📦 Wheel file | *Building...* |\n" + "| 📦 \`@streamlit/component-v2-lib\` | *Building...* |\n" + "| 🕹️ Preview app | *Building...* |"; // Find an existing comment; if it exists, we will update it const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number }); const botComment = comments.find(comment => comment.user.login === 'github-actions[bot]' && (comment.body.includes('PR preview is building') || comment.body.includes('PR preview is ready')) ); let commentBody; if (botComment) { // Get the current comment content to check if it has links const { data: currentComment } = await github.rest.issues.getComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: botComment.id }); // Check if the comment already has links (not just "Building...") if (currentComment.body.includes("https://core-previews.s3-us-west-2.amazonaws.com/")) { // If links exist, just update the status header commentBody = currentComment.body.replace("### ✅ PR preview is ready!", "### 🚧 PR preview is building..."); } else { // If no links, use the building template commentBody = buildingTemplate; } // Update existing comment await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: botComment.id, body: commentBody }); console.log('Updated existing comment with ID:', botComment.id); } else { // Create new comment with building template commentBody = buildingTemplate; const { data: newComment } = await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: commentBody }); console.log('Created new comment with ID:', newComment.id); } analyze-frontend-bundle: runs-on: ubuntu-latest steps: - name: Checkout Streamlit code uses: actions/checkout@v6 with: persist-credentials: false submodules: "recursive" fetch-depth: 2 - name: Set Python version vars uses: ./.github/actions/build_info - name: Setup virtual env uses: ./.github/actions/make_init with: python_version: ${{ env.PYTHON_MAX_VERSION }} - name: Build Frontend & Analyze env: ANALYZE_BUNDLE: "true" run: | make frontend - name: Upload Bundle Stats JSON uses: actions/upload-artifact@v6 with: name: bundle_analysis_json path: frontend/app/bundle-analysis.json - name: Upload Bundle Stats HTML uses: actions/upload-artifact@v6 with: name: bundle_analysis_html path: frontend/app/bundle-analysis.html upload-whl: runs-on: ubuntu-latest permissions: contents: read # This is required for actions/checkout statuses: write # This is required for "Set S3 URL as Github status" step defaults: run: shell: bash outputs: enable-setup: ${{ steps.exports.outputs.enable-setup }} preview-branch: ${{ steps.exports.outputs.preview-branch }} s3-url: ${{ steps.exports.outputs.s3-url }} steps: - name: Checkout Streamlit code uses: actions/checkout@v6 with: persist-credentials: false submodules: "recursive" fetch-depth: 2 - name: Set Python version vars uses: ./.github/actions/build_info - name: Setup virtual env uses: ./.github/actions/make_init with: python_version: ${{ env.PYTHON_MAX_VERSION }} - name: Fix apt mirrors if: ${{ runner.os == 'Linux' }} uses: ./.github/actions/apt_mirror_fix - name: Create Wheel File timeout-minutes: 120 run: | sudo apt update sudo apt install rsync jq make package INDEX_NAME=$(jq '."index.html".name' lib/streamlit/static/manifest.json | tr -d '"') if [[ "${INDEX_NAME}" != "index" ]] then echo 'lib/streamlit/static/manifest.json is missing or malformatted!' exit 1 fi - name: Store Whl File uses: actions/upload-artifact@v6 with: name: whl_file path: lib/dist/*.whl # Uses action to safely process user input (branch name) to prevent script injection attacks - name: Set Environment Variables uses: ./.github/actions/preview_branch with: pull_request_number: ${{ github.event.pull_request.number }} ref_type: ${{ github.ref_type }} branch: ${{ github.ref_name }} - if: ${{ env.AWS_ACCESS_KEY_ID != '' }} name: Upload wheel to S3 id: exports env: BRANCH: ${{ env.BRANCH }} PREVIEW_BRANCH: ${{ env.PREVIEW_BRANCH }} AWS_DEFAULT_REGION: us-west-2 AWS_ACCESS_KEY_ID: ${{ secrets.CORE_PREVIEWS_S3_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.CORE_PREVIEWS_S3_SECRET_KEY }} # To create a consistent location for the release/demo whl file, we need a # stagnent version number (streamlit-11.11.11) run: | # Install awscli via uv: uv pip install awscli==1.37.6 cd lib/dist export WHEELFILE="$(ls -t *.whl | head -n 1)" if [ "${BRANCH}" = "release/demo" ] then aws s3 cp "${WHEELFILE}" s3://core-previews/${PREVIEW_BRANCH}/streamlit-11.11.11-py2.py3-none-any.whl --acl public-read S3_URL="https://core-previews.s3-us-west-2.amazonaws.com/${PREVIEW_BRANCH}/streamlit-11.11.11-py2.py3-none-any.whl" else aws s3 cp "${WHEELFILE}" s3://core-previews/${PREVIEW_BRANCH}/ --acl public-read S3_URL="https://core-previews.s3-us-west-2.amazonaws.com/${PREVIEW_BRANCH}/${WHEELFILE}" fi echo -e "Wheel file download link: ${S3_URL}" cd ../.. # env variables don't carry over between gh action jobs echo "enable-setup=${{ env.AWS_ACCESS_KEY_ID != '' }}" >> $GITHUB_OUTPUT echo "preview-branch=$PREVIEW_BRANCH" >> $GITHUB_OUTPUT echo "s3-url=${S3_URL}" >> $GITHUB_OUTPUT - if: steps.exports.outputs.enable-setup == 'true' && success() && github.repository == 'streamlit/streamlit' && github.event_name == 'pull_request' name: Set S3 URL as Github Status uses: actions/github-script@v8 env: S3_URL: ${{ steps.exports.outputs.s3-url }} with: script: | const { sha } = context.payload.pull_request.head // For API documentation, see: // https://docs.github.com/en/rest/commits/statuses?apiVersion=2022-11-28#create-a-commit-status await github.request(`POST /repos/streamlit/streamlit/statuses/{sha}`, { owner: 'streamlit', repo: 'streamlit', sha, state: 'success', target_url: process.env.S3_URL, context: 'Wheel ready!' }) check-size: runs-on: ubuntu-latest needs: upload-whl if: github.event_name == 'pull_request' && github.repository == 'streamlit/streamlit' continue-on-error: true # Ensure the job doesn't fail the workflow permissions: contents: read pull-requests: write # Required to add comments to PR actions: read # Required to access workflow runs env: SIZE_CHANGE_THRESHOLD: 0.25 # Percentage threshold for significant size changes WHEEL_ARTIFACT_NAME: whl_file # Name of the wheel artifact steps: - name: Compare wheel sizes id: compare-sizes uses: actions/github-script@v8 with: script: | const threshold = parseFloat(process.env.SIZE_CHANGE_THRESHOLD); const wheelArtifactName = process.env.WHEEL_ARTIFACT_NAME; console.log(`Threshold for significant change: ${threshold}%`); console.log(`Looking for wheel artifact with name: ${wheelArtifactName}`); try { // Get the current workflow run ID and artifacts const currentRunId = context.runId; console.log(`Current run ID: ${currentRunId}`); const { data: currentArtifacts } = await github.rest.actions.listWorkflowRunArtifacts({ owner: context.repo.owner, repo: context.repo.repo, run_id: currentRunId }); const currentWhlArtifact = currentArtifacts.artifacts.find(artifact => artifact.name === wheelArtifactName); if (!currentWhlArtifact) { throw new Error(`No ${wheelArtifactName} artifact found in the current workflow run`); } const currentSize = currentWhlArtifact.size_in_bytes; console.log(`Current wheel size: ${currentSize} bytes (${(currentSize / 1024).toFixed(2)} KB)`); // Get the latest successful workflow run on develop branch const { data: runs } = await github.rest.actions.listWorkflowRuns({ owner: context.repo.owner, repo: context.repo.repo, workflow_id: 'pr-preview.yml', branch: 'develop', status: 'success', per_page: 1 }); if (runs.workflow_runs.length === 0) { throw new Error('No successful workflow runs found on develop branch'); } const latestRun = runs.workflow_runs[0]; console.log(`Latest successful run on develop: ${latestRun.id}`); // Get artifacts for the develop run const { data: developArtifacts } = await github.rest.actions.listWorkflowRunArtifacts({ owner: context.repo.owner, repo: context.repo.repo, run_id: latestRun.id }); const developWhlArtifact = developArtifacts.artifacts.find(artifact => artifact.name === wheelArtifactName); if (!developWhlArtifact) { throw new Error(`No ${wheelArtifactName} artifact found in the develop workflow run`); } const developSize = developWhlArtifact.size_in_bytes; console.log(`Develop wheel size: ${developSize} bytes (${(developSize / 1024).toFixed(2)} KB)`); // Calculate percentage difference const diffPercent = ((currentSize - developSize) / developSize) * 100; const absDiffPercent = Math.abs(diffPercent); console.log(`Size difference: ${diffPercent.toFixed(2)}% (absolute: ${absDiffPercent.toFixed(2)}%)`); // Check if difference is significant (>= threshold%) const isSignificant = absDiffPercent >= threshold; // Format sizes for human readability const currentSizeKB = (currentSize / 1024).toFixed(2); const developSizeKB = (developSize / 1024).toFixed(2); return { success: true, is_significant: isSignificant, diff_percent: diffPercent, current_size_kb: currentSizeKB, develop_size_kb: developSizeKB, threshold: threshold }; } catch (error) { console.error(`Error comparing wheel sizes: ${error.message}`); // Let the step fail naturally throw error; } env: SIZE_CHANGE_THRESHOLD: ${{ env.SIZE_CHANGE_THRESHOLD }} WHEEL_ARTIFACT_NAME: ${{ env.WHEEL_ARTIFACT_NAME }} - name: Add PR comment for significant size change if: success() && fromJSON(steps.compare-sizes.outputs.result).is_significant uses: actions/github-script@v8 with: script: | const result = JSON.parse(process.env.COMPARE_RESULT); const diffPercent = result.diff_percent; const currentSizeKB = result.current_size_kb; const developSizeKB = result.develop_size_kb; const threshold = result.threshold; const changeType = diffPercent > 0 ? 'increased' : 'decreased'; const emoji = diffPercent > 0 ? '📈' : '📉'; const commentBody = `### ${emoji} Significant wheel size change detected The wheel file size has **${changeType} by ${Math.abs(diffPercent).toFixed(2)}%** (threshold: ${threshold}%) - Current PR: ${currentSizeKB} KB - Latest develop: ${developSizeKB} KB Please verify that this change is expected.`; // Unique identifier for our comment const commentIdentifier = '<!-- STREAMLIT-WHEEL-SIZE-CHECK -->'; const fullCommentBody = `${commentIdentifier}\n${commentBody}`; try { // Get all comments on the PR const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, }); // Look for our previous wheel size comment const sizeComment = comments.find(comment => comment.body.includes(commentIdentifier)); if (sizeComment) { // Update existing comment await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: sizeComment.id, body: fullCommentBody }); console.log('Updated existing wheel size comment'); } else { // Create new comment await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: fullCommentBody }); console.log('Created new wheel size comment'); } } catch (error) { console.error(`Error adding PR comment: ${error.message}`); throw error; // Let the step fail naturally } env: COMPARE_RESULT: ${{ steps.compare-sizes.outputs.result }} check-bundle-size: runs-on: ubuntu-latest needs: analyze-frontend-bundle if: github.event_name == 'pull_request' && github.repository == 'streamlit/streamlit' continue-on-error: true permissions: contents: read pull-requests: write actions: read env: BUNDLE_SIZE_CHANGE_PERCENTAGE_THRESHOLD: 0.5 steps: - name: Download current bundle analysis uses: actions/download-artifact@v7 with: name: bundle_analysis_json path: current_bundle - name: Get latest develop run ID id: get-latest-run uses: actions/github-script@v8 with: script: | const { data: runs } = await github.rest.actions.listWorkflowRuns({ owner: context.repo.owner, repo: context.repo.repo, workflow_id: 'pr-preview.yml', branch: 'develop', status: 'success', per_page: 1 }); if (runs.workflow_runs.length === 0) { core.warning('No successful workflow runs found on develop branch'); return; } core.setOutput('run_id', runs.workflow_runs[0].id); - name: Download develop bundle analysis uses: actions/download-artifact@v7 continue-on-error: true with: repository: streamlit/streamlit run-id: ${{ steps.get-latest-run.outputs.run_id }} name: bundle_analysis_json path: develop_bundle github-token: ${{ secrets.GITHUB_TOKEN }} - name: Compare bundle sizes and comment uses: actions/github-script@v8 with: script: | const fs = require('fs'); const path = require('path'); const loadBundle = (dir) => { try { const jsonPath = path.join(process.cwd(), dir, 'bundle-analysis.json'); if (fs.existsSync(jsonPath)) { return JSON.parse(fs.readFileSync(jsonPath, 'utf8')); } console.log(`File not found: ${jsonPath}`); return null; } catch (e) { console.log(`Error loading bundle from ${dir}:`, e.message); return null; } }; const currentData = loadBundle('current_bundle'); const developData = loadBundle('develop_bundle'); if (!currentData) { console.log('Missing current bundle data, skipping comparison.'); return; } if (!developData) { console.log('Missing develop bundle data, skipping comparison.'); return; } const calculateMetrics = (data) => { let totalGzip = 0; let entryGzip = 0; if (data) { for (const item of data) { totalGzip += (item.gzipSize || 0); if (item.isEntry) { entryGzip += (item.gzipSize || 0); } } } return { totalGzip, entryGzip }; }; const formatBytes = (bytes) => { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KiB', 'MiB', 'GiB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; const currentMetrics = calculateMetrics(currentData); const developMetrics = calculateMetrics(developData); const threshold = parseFloat(process.env.BUNDLE_SIZE_CHANGE_PERCENTAGE_THRESHOLD); const checkSignificant = (current, develop) => { if (develop === 0) { return current === 0 ? { diff: 0, percent: 0, isSignificant: false } : { diff: current, percent: 100, isSignificant: true }; } const diff = current - develop; const percent = (diff / develop) * 100; return { diff, percent, isSignificant: Math.abs(percent) >= threshold }; }; const total = checkSignificant(currentMetrics.totalGzip, developMetrics.totalGzip); const entry = checkSignificant(currentMetrics.entryGzip, developMetrics.entryGzip); console.log(`Total Gzip: Current=${formatBytes(currentMetrics.totalGzip)}, Develop=${formatBytes(developMetrics.totalGzip)}, Change=${total.percent.toFixed(2)}%`); console.log(`Entry Gzip: Current=${formatBytes(currentMetrics.entryGzip)}, Develop=${formatBytes(developMetrics.entryGzip)}, Change=${entry.percent.toFixed(2)}%`); const isSignificant = total.isSignificant || entry.isSignificant; const isIncreased = total.percent > 0 || entry.percent > 0; // Find existing comment const commentIdentifier = '<!-- STREAMLIT-BUNDLE-SIZE-CHECK -->'; const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, }); const existingComment = comments.find(comment => comment.body.includes(commentIdentifier)); if (isSignificant || existingComment) { const emoji = isIncreased ? '📈' : '📉'; const formatChange = (res) => { const sign = res.percent > 0 ? '+' : ''; return `${sign}${res.percent.toFixed(2)}%`; }; const header = isSignificant ? `### ${emoji} Significant bundle size change detected` : `### ✅ Bundle size change is within normal range`; const lines = [ header, '', '| Metric | This Branch | `develop` | Change (%) |', '| :--- | :--- | :--- | :--- |', `| **Total (gzip)** | ${formatBytes(currentMetrics.totalGzip)} | ${formatBytes(developMetrics.totalGzip)} | ${formatChange(total)} |`, `| **Entry (gzip)** | ${formatBytes(currentMetrics.entryGzip)} | ${formatBytes(developMetrics.entryGzip)} | ${formatChange(entry)} |`, '' ]; if (isSignificant) { lines.push('Please verify that this change is expected.', ''); } lines.push( '', `📊 [View detailed bundle comparison](https://issues.streamlit.app/frontend_bundle_analysis?pr=${context.issue.number})` ); const commentBody = lines.join('\n'); const fullCommentBody = `${commentIdentifier}\n${commentBody}`; if (existingComment) { await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: existingComment.id, body: fullCommentBody }); console.log('Updated existing comment'); } else { await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: fullCommentBody }); console.log('Created new comment'); } } else { console.log('Changes within threshold and no existing comment, skipping.'); } build-component-v2-lib: runs-on: ubuntu-latest continue-on-error: true # Ensure failure doesn't block update-pr-comment job permissions: contents: read # This is required for actions/checkout defaults: run: shell: bash outputs: package-built: ${{ steps.build-package.outputs.package-built }} steps: - name: Checkout Streamlit code uses: actions/checkout@v6 with: persist-credentials: false fetch-depth: 1 - name: Enable corepack run: corepack enable - name: Set up Node.js uses: actions/setup-node@v6 with: node-version-file: ".nvmrc" cache: "yarn" cache-dependency-path: "**/yarn.lock" - name: Install dependencies working-directory: frontend/component-v2-lib run: yarn install --frozen-lockfile - name: Build package id: build-package working-directory: frontend/component-v2-lib run: | echo "Building @streamlit/component-v2-lib..." yarn build echo "Creating npm package..." npm pack # List the created package for verification echo "Created package:" ls -la *.tgz # Set output to indicate package was built echo "package-built=true" >> $GITHUB_OUTPUT - name: Store npm package uses: actions/upload-artifact@v6 with: name: component-v2-lib-package path: frontend/component-v2-lib/*.tgz retention-days: 30 setup-preview: runs-on: ubuntu-latest needs: upload-whl if: needs.upload-whl.outputs.enable-setup == 'true' defaults: run: shell: bash outputs: deploy-url: ${{ steps.deploy-url.outputs.deploy-url }} steps: - name: Checkout Core Previews Repo uses: actions/checkout@v6 with: repository: streamlit/core-previews # The default GITHUB_TOKEN is scoped only to the triggering streamlit/streamlit repo. # Accessing streamlit/core-previews repo requires a separate auth token. token: ${{ secrets.CORE_PREVIEWS_REPO_TOKEN }} # Save the access token to the local git config, so # later git commands can work. persist-credentials: true - name: Setup preview repo env: PREVIEW_BRANCH: ${{ needs.upload-whl.outputs.preview-branch }} S3_URL: ${{ needs.upload-whl.outputs.s3-url }} run: | git config --global user.email "core+streamlitbot-github@streamlit.io" git config --global user.name "Streamlit Bot" git branch -D "$PREVIEW_BRANCH" &>/dev/null || true git checkout -b "$PREVIEW_BRANCH" echo "$S3_URL" >> requirements.txt git add . git commit -m "Prepare core preview: ${PREVIEW_BRANCH}" git push -f origin ${PREVIEW_BRANCH} - name: Ready to deploy! id: deploy-url env: PREVIEW_BRANCH: ${{ needs.upload-whl.outputs.preview-branch }} run: | DEPLOY_URL="https://share.streamlit.io/deploy?repository=streamlit/core-previews&branch=${PREVIEW_BRANCH}&mainModule=E2E_Tester_🧪.py&subdomain=${PREVIEW_BRANCH}" echo -e "${DEPLOY_URL}" echo "deploy-url=${DEPLOY_URL}" >> $GITHUB_OUTPUT update-pr-comment: runs-on: ubuntu-latest needs: [initial-pr-comment, upload-whl, build-component-v2-lib, setup-preview] # Only run on PRs that are not from forks if: needs.upload-whl.outputs.enable-setup == 'true' && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository permissions: pull-requests: write steps: - name: Checkout repository uses: actions/checkout@v6 with: fetch-depth: 1 - name: Update comment (Ready) uses: actions/github-script@v8 with: script: | // Find the existing comment const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number }); const botComment = comments.find(comment => comment.user.login === 'github-actions[bot]' && (comment.body.includes('PR preview is building') || comment.body.includes('PR preview is ready')) ); if (!botComment) { throw new Error('No PR preview comment found. The initial comment job may have failed.'); } const commentId = botComment.id; console.log('Found comment with ID:', commentId); const s3Url = '${{ needs.upload-whl.outputs.s3-url }}'; const deployUrl = '${{ needs.setup-preview.outputs.deploy-url }}'; const previewBranch = '${{ needs.upload-whl.outputs.preview-branch }}'; const viewUrl = `${previewBranch}.streamlit.app`; // Define the ready template once const componentPackageBuilt = '${{ needs.build-component-v2-lib.outputs.package-built }}'; const workflowRunUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; const componentRow = componentPackageBuilt === 'true' ? `| 📦 \`@streamlit/component-v2-lib\` | [Download from artifacts](${workflowRunUrl}) |\n` : `| 📦 \`@streamlit/component-v2-lib\` | *Build failed* |\n`; const readyTemplate = "### ✅ PR preview is ready!\n\n" + "| Name | Link |\n" + "| :----: | ---- |\n" + "| 📦 Wheel file | " + s3Url + " |\n" + componentRow + "| 🕹️ Preview app | [" + viewUrl + "](https://" + viewUrl + ") (☁️ Deploy [here](" + deployUrl + ") if not accessible)| "; await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: commentId, body: readyTemplate }); console.log('Successfully updated comment with ID:', commentId);