-
Notifications
You must be signed in to change notification settings - Fork 13.7k
Add WebUI auto-build workflow #17217
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,261 @@ | ||
| name: WebUI Auto-Build | ||
|
|
||
| on: | ||
| pull_request: | ||
| types: [opened, synchronize, reopened] | ||
| paths: | ||
| - ".github/workflows/webui-auto-build.yml" | ||
| - "tools/server/webui/**" | ||
| - "!tools/server/webui/README.md" | ||
| - "!tools/server/public/index.html.gz" | ||
|
|
||
| push: | ||
| branches: | ||
| - master | ||
| paths: | ||
| - ".github/workflows/webui-auto-build.yml" | ||
| - "tools/server/webui/**" | ||
| - "!tools/server/webui/README.md" | ||
| - "!tools/server/public/index.html.gz" | ||
|
|
||
| workflow_dispatch: | ||
| inputs: | ||
| pr_number: | ||
| description: "PR number to rebuild (leave empty for all open PRs)" | ||
| required: false | ||
| type: string | ||
|
|
||
| permissions: | ||
| contents: write | ||
| pull-requests: write | ||
|
|
||
| concurrency: | ||
| group: webui-auto-build-${{ github.event.pull_request.number || github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| identify-prs: | ||
| name: Identify PRs to Rebuild | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| pr_numbers: ${{ steps.get-prs.outputs.pr_numbers }} | ||
| steps: | ||
| - name: Get PR numbers | ||
| id: get-prs | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| let prNumbers = []; | ||
|
|
||
| // If triggered by a PR event, only rebuild that PR | ||
| if (context.eventName === 'pull_request') { | ||
| prNumbers = [context.payload.pull_request.number]; | ||
| } | ||
| // If manually triggered with a specific PR number | ||
| else if (context.eventName === 'workflow_dispatch' && context.payload.inputs.pr_number) { | ||
| prNumbers = [parseInt(context.payload.inputs.pr_number)]; | ||
| } | ||
| // If triggered by master push or manual without PR number, rebuild all open PRs | ||
| else { | ||
| const { data: pullRequests } = await github.rest.pulls.list({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| state: 'open', | ||
| per_page: 100 | ||
| }); | ||
|
|
||
| // Filter PRs that have webui changes | ||
| for (const pr of pullRequests) { | ||
| const { data: files } = await github.rest.pulls.listFiles({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| pull_number: pr.number | ||
| }); | ||
|
|
||
| const hasWebuiChanges = files.some(file => | ||
| file.filename.startsWith('tools/server/webui/') && | ||
| !file.filename.endsWith('README.md') && | ||
| file.filename !== 'tools/server/public/index.html.gz' | ||
| ); | ||
|
|
||
| if (hasWebuiChanges) { | ||
| prNumbers.push(pr.number); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| console.log(`PRs to rebuild: ${prNumbers.join(', ')}`); | ||
| core.setOutput('pr_numbers', JSON.stringify(prNumbers)); | ||
|
|
||
| rebuild-webui: | ||
| name: Rebuild WebUI for PR #${{ matrix.pr_number }} | ||
| needs: identify-prs | ||
| if: needs.identify-prs.outputs.pr_numbers != '[]' | ||
| runs-on: ubuntu-latest | ||
| strategy: | ||
| matrix: | ||
| pr_number: ${{ fromJson(needs.identify-prs.outputs.pr_numbers) }} | ||
| fail-fast: false | ||
| max-parallel: 3 | ||
|
|
||
| steps: | ||
| - name: Get PR details | ||
| id: pr-details | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| const { data: pr } = await github.rest.pulls.get({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| pull_number: ${{ matrix.pr_number }} | ||
| }); | ||
|
|
||
| core.setOutput('head_ref', pr.head.ref); | ||
| core.setOutput('head_sha', pr.head.sha); | ||
| core.setOutput('head_repo', pr.head.repo.full_name); | ||
| core.setOutput('base_ref', pr.base.ref); | ||
| core.setOutput('is_fork', pr.head.repo.full_name !== context.repo.owner + '/' + context.repo.repo); | ||
|
|
||
| - name: Checkout PR branch | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| repository: ${{ steps.pr-details.outputs.head_repo }} | ||
| ref: ${{ steps.pr-details.outputs.head_ref }} | ||
| token: ${{ secrets.PAT_TOKEN || secrets.GITHUB_TOKEN }} | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: "22" | ||
| cache: "npm" | ||
| cache-dependency-path: "tools/server/webui/package-lock.json" | ||
|
|
||
| - name: Install dependencies | ||
| run: npm ci | ||
| working-directory: tools/server/webui | ||
|
|
||
| - name: Build WebUI | ||
| run: npm run build | ||
| working-directory: tools/server/webui | ||
|
|
||
| - name: Check for changes | ||
| id: check-changes | ||
| run: | | ||
| # Decompress the newly built file and compute its hash | ||
| NEW_HASH=$(gunzip -c tools/server/public/index.html.gz | sha256sum | cut -d' ' -f1) | ||
| echo "New build hash: $NEW_HASH" | ||
|
|
||
| # Get the original file from git, decompress it, and compute its hash | ||
| if git show HEAD:tools/server/public/index.html.gz 2>/dev/null | gunzip -c | sha256sum > /tmp/old-hash.txt 2>&1; then | ||
| OLD_HASH=$(cut -d' ' -f1 /tmp/old-hash.txt) | ||
| echo "Original hash: $OLD_HASH" | ||
|
|
||
| # Compare hashes | ||
| if [ "$NEW_HASH" = "$OLD_HASH" ]; then | ||
| echo "has_changes=false" >> $GITHUB_OUTPUT | ||
| echo "✓ No changes detected in static output (HTML content is identical)" | ||
| else | ||
| echo "has_changes=true" >> $GITHUB_OUTPUT | ||
| echo "✓ Changes detected in static output" | ||
| fi | ||
| else | ||
| # File doesn't exist in repo yet (new file) | ||
| echo "has_changes=true" >> $GITHUB_OUTPUT | ||
| echo "✓ New static output file detected" | ||
| fi | ||
|
|
||
| # Cleanup | ||
| rm -f /tmp/old-hash.txt | ||
|
|
||
| - name: Commit and push changes | ||
| if: steps.check-changes.outputs.has_changes == 'true' | ||
| id: commit-changes | ||
| run: | | ||
| git config user.name "github-actions[bot]" | ||
| git config user.email "github-actions[bot]@users.noreply.github.com" | ||
|
|
||
| git add tools/server/public/index.html.gz | ||
| git commit -m "chore(webui): auto-rebuild static output [skip ci]" | ||
|
|
||
| # Try to push, capture exit code | ||
| if git push origin ${{ steps.pr-details.outputs.head_ref }}; then | ||
| echo "push_success=true" >> $GITHUB_OUTPUT | ||
| else | ||
| echo "push_success=false" >> $GITHUB_OUTPUT | ||
| echo "::warning::Failed to push changes. This is expected for fork PRs without PAT_TOKEN configured." | ||
| fi | ||
|
|
||
| - name: Add comment to PR | ||
| if: steps.check-changes.outputs.has_changes == 'true' | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| const { data: comments } = await github.rest.issues.listComments({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: ${{ matrix.pr_number }} | ||
| }); | ||
|
|
||
| const pushSuccess = '${{ steps.commit-changes.outputs.push_success }}' === 'true'; | ||
| const isFork = '${{ steps.pr-details.outputs.is_fork }}' === 'true'; | ||
|
|
||
| // Check if we already commented about auto-rebuild | ||
| const botComment = comments.find(comment => | ||
| comment.user.type === 'Bot' && | ||
| comment.body.includes('🤖 WebUI static output') | ||
| ); | ||
|
|
||
| let message; | ||
| if (pushSuccess) { | ||
| message = `🤖 **WebUI static output auto-rebuilt** | ||
|
|
||
| The static build has been automatically updated to reflect the latest changes. | ||
|
|
||
| <details> | ||
| <summary>Build details</summary> | ||
|
|
||
| - **Workflow run**: [#${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While these variables are controlled by us, I think it's safer to use env instead. This will prevent any kind of injection attack in the future Even better, put this script to a dedicated |
||
| - **Commit**: ${{ steps.pr-details.outputs.head_sha }} | ||
| - **Triggered by**: ${context.eventName === 'pull_request' ? 'PR update' : 'master branch update'} | ||
|
|
||
| </details>`; | ||
| } else { | ||
| message = `🤖 **WebUI static output needs rebuild** | ||
|
|
||
| The static build was generated successfully, but could not be automatically committed${isFork ? ' (fork PR requires PAT_TOKEN)' : ''}. | ||
|
|
||
| **Action required:** Please run \`npm run build\` locally in \`tools/server/webui/\` and commit the updated \`tools/server/public/index.html.gz\`. | ||
|
|
||
| <details> | ||
| <summary>Build details</summary> | ||
|
|
||
| - **Workflow run**: [#${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) | ||
| - **Status**: Build succeeded, push failed | ||
| ${isFork ? '- **Note**: Fork PRs require a PAT_TOKEN secret to auto-commit' : ''} | ||
|
|
||
| </details>`; | ||
| } | ||
|
|
||
| if (botComment) { | ||
| // Update existing comment | ||
| await github.rest.issues.updateComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| comment_id: botComment.id, | ||
| body: message | ||
| }); | ||
| } else { | ||
| // Create new comment | ||
| await github.rest.issues.createComment({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| issue_number: ${{ matrix.pr_number }}, | ||
| body: message | ||
| }); | ||
| } | ||
|
|
||
| - name: Report no changes | ||
| if: steps.check-changes.outputs.has_changes == 'false' | ||
| run: | | ||
| echo "✓ Static output is already up to date" | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we simplify this step by using
git status? Example:llama.cpp/.github/workflows/check-vendor.yml
Lines 40 to 52 in a19bd6f