[net11.0] Update dependencies from dotnet/dotnet #3505
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
| # Copyright (c) Microsoft Corporation. | |
| # Licensed under the MIT License. | |
| # This workflow applies updated expected app size files from a GitHub gist | |
| # when a maintainer comments '/apply-gist <gist-url>' on a pull request. | |
| # | |
| # Security: | |
| # - Only honors comments from users with write access to the repository | |
| # - Only accepts gists from approved owners (see APPROVED_GIST_OWNERS below) | |
| name: Apply Gist | |
| on: | |
| issue_comment: | |
| types: [created] | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| issues: write | |
| jobs: | |
| apply-gist: | |
| if: >- | |
| github.event.issue.pull_request && | |
| startsWith(github.event.comment.body, '/apply-gist ') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check commenter permissions | |
| id: check-permissions | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| username: context.payload.comment.user.login, | |
| }); | |
| const level = permission.permission; | |
| if (level !== 'admin' && level !== 'write' && level !== 'maintain') { | |
| core.setFailed(`User '${context.payload.comment.user.login}' does not have write access (has '${level}'). Ignoring.`); | |
| return; | |
| } | |
| core.info(`User '${context.payload.comment.user.login}' has '${level}' access. Proceeding.`); | |
| - name: React to comment | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| await github.rest.reactions.createForIssueComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: context.payload.comment.id, | |
| content: 'rocket', | |
| }); | |
| - name: Parse gist URL | |
| id: parse | |
| env: | |
| COMMENT_BODY: ${{ github.event.comment.body }} | |
| run: | | |
| GIST_URL=$(echo "$COMMENT_BODY" | grep -oP '(?<=/apply-gist\s)https://gist\.github\.com/\S+') | |
| if [ -z "$GIST_URL" ]; then | |
| echo "::error::Could not parse a valid gist URL from the comment." | |
| exit 1 | |
| fi | |
| # Extract gist ID (last path component, strip any trailing slash) | |
| GIST_ID=$(echo "$GIST_URL" | sed 's|/$||' | awk -F/ '{print $NF}') | |
| if [ -z "$GIST_ID" ]; then | |
| echo "::error::Could not extract gist ID from URL: $GIST_URL" | |
| exit 1 | |
| fi | |
| echo "gist_url=$GIST_URL" >> "$GITHUB_OUTPUT" | |
| echo "gist_id=$GIST_ID" >> "$GITHUB_OUTPUT" | |
| - name: Validate gist owner | |
| id: validate-owner | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| GIST_ID: ${{ steps.parse.outputs.gist_id }} | |
| with: | |
| script: | | |
| // Approved gist owners. Add more entries here as needed. | |
| const approvedOwners = [ | |
| 'vs-mobiletools-engineering-service2', | |
| ]; | |
| // The Gists API isn't accessible to the GitHub Actions token (a GitHub App | |
| // installation token), so fetch the gist unauthenticated instead. Secret gists | |
| // are readable by anyone who knows the id, which is all we need here. | |
| const resp = await fetch(`https://api.github.com/gists/${process.env.GIST_ID}`, { | |
| headers: { | |
| 'Accept': 'application/vnd.github+json', | |
| 'X-GitHub-Api-Version': '2022-11-28', | |
| 'User-Agent': 'dotnet-macios-apply-gist', | |
| }, | |
| }); | |
| if (!resp.ok) { | |
| const body = await resp.text().catch(() => ''); | |
| core.setFailed(`Failed to fetch gist '${process.env.GIST_ID}': ${resp.status} ${resp.statusText}${body ? `\n${body}` : ''}`); | |
| return; | |
| } | |
| const gist = await resp.json(); | |
| if (!gist.owner) { | |
| core.setFailed('Gist has no owner (anonymous or deleted). Cannot verify ownership.'); | |
| return; | |
| } | |
| const owner = gist.owner.login; | |
| if (!approvedOwners.includes(owner)) { | |
| core.setFailed(`Gist owner '${owner}' is not in the approved list: [${approvedOwners.join(', ')}]`); | |
| return; | |
| } | |
| core.info(`Gist owner '${owner}' is approved.`); | |
| - name: Get PR branch | |
| id: pr-branch | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const { data: pr } = await github.rest.pulls.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: context.issue.number, | |
| }); | |
| if (pr.head.repo.full_name !== pr.base.repo.full_name) { | |
| core.setFailed(`Cannot apply gist to fork PRs (head repo: ${pr.head.repo.full_name}). Push the updated files manually.`); | |
| return; | |
| } | |
| core.setOutput('ref', pr.head.ref); | |
| core.setOutput('repo_full_name', pr.head.repo.full_name); | |
| - name: Checkout PR branch | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ steps.pr-branch.outputs.ref }} | |
| repository: ${{ steps.pr-branch.outputs.repo_full_name }} | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| persist-credentials: true | |
| - name: Download and apply gist diffs | |
| env: | |
| GIST_ID: ${{ steps.parse.outputs.gist_id }} | |
| run: | | |
| EXPECTED_DIR="tests/dotnet/UnitTests/expected" | |
| mkdir -p "$EXPECTED_DIR" | |
| DIFF_DIR="$(mktemp -d)" | |
| export DIFF_DIR EXPECTED_DIR | |
| # Download gist metadata to get file URLs. The Gists API isn't accessible to the GitHub Actions token | |
| # (a GitHub App installation token), so fetch it unauthenticated. | |
| if ! GIST_JSON=$(curl -fSL -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" -H "User-Agent: dotnet-macios-apply-gist" "https://api.github.com/gists/$GIST_ID"); then | |
| echo "Failed to fetch gist '$GIST_ID' (404/403/rate limit?)." >&2 | |
| exit 1 | |
| fi | |
| # Newer builds upload a unified diff ('<name>.txt.diff') for each changed expected file, instead of the | |
| # entire (potentially very large) expected file. For backwards compatibility we still accept gists that | |
| # contain the full expected files ('<name>.txt'). Download and validate everything, applying diffs and | |
| # writing full files below. | |
| echo "$GIST_JSON" | python3 -c " | |
| import json, sys, urllib.request, os, re | |
| gist = json.load(sys.stdin) | |
| diff_dir = os.environ['DIFF_DIR'] | |
| expected_dir = os.environ['EXPECTED_DIR'] | |
| files = gist['files'] | |
| # Allowed expected-file name, and the same name with a '.diff' suffix (a unified diff of that file). | |
| allowed_diff = re.compile(r'^[A-Za-z0-9]+-[A-Za-z0-9-]+-(size|preservedapis)\.txt\.diff$') | |
| allowed_full = re.compile(r'^[A-Za-z0-9]+-[A-Za-z0-9-]+-(size|preservedapis)\.txt$') | |
| applied = 0 | |
| for filename, file_info in files.items(): | |
| is_diff = bool(allowed_diff.match(filename)) | |
| is_full = bool(allowed_full.match(filename)) | |
| if not is_diff and not is_full: | |
| print(f'Skipping file with unexpected name: {filename}') | |
| continue | |
| content = file_info.get('content') | |
| if content is None: | |
| # Large files need to be fetched from raw_url | |
| raw_url = file_info['raw_url'] | |
| content = urllib.request.urlopen(raw_url).read().decode('utf-8') | |
| # Gist content can be served with CRLF line endings; normalize to LF | |
| # so 'git apply' doesn't fail against the repo's LF expected files. | |
| content = content.replace('\r\n', '\n').replace('\r', '\n') | |
| if is_diff: | |
| # The diff for '<name>.txt.diff' is only allowed to touch 'tests/dotnet/UnitTests/expected/<name>.txt'. | |
| expected_target = 'tests/dotnet/UnitTests/expected/' + filename[:-len('.diff')] | |
| allowed_targets = ('/dev/null', 'a/' + expected_target, 'b/' + expected_target) | |
| for line in content.splitlines(): | |
| if line.startswith('--- ') or line.startswith('+++ '): | |
| # Strip a trailing tab + timestamp if present, then verify the target path. | |
| target = line[4:].split('\t', 1)[0].strip() | |
| if target not in allowed_targets: | |
| print(f'Rejecting diff {filename}: unexpected target path: {target}') | |
| sys.exit(1) | |
| dest = os.path.join(diff_dir, filename) | |
| with open(dest, 'w') as f: | |
| f.write(content) | |
| print(f'Downloaded diff: {filename}') | |
| else: | |
| # Full expected file: write it directly into the expected directory. | |
| dest = os.path.join(expected_dir, filename) | |
| with open(dest, 'w') as f: | |
| f.write(content) | |
| print(f'Applied full file: {filename}') | |
| applied += 1 | |
| if applied == 0: | |
| print('No applicable files found in gist.') | |
| sys.exit(1) | |
| " | |
| # Apply each diff. 'git apply' handles both modifications of existing files and creation of new files. | |
| shopt -s nullglob | |
| for diff in "$DIFF_DIR"/*.diff; do | |
| echo "Applying $(basename "$diff")..." | |
| git apply -p1 --verbose "$diff" | |
| done | |
| - name: Commit and push | |
| env: | |
| GIST_URL: ${{ steps.parse.outputs.gist_url }} | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add tests/dotnet/UnitTests/expected/ | |
| if git diff --cached --quiet; then | |
| echo "No changes to commit." | |
| exit 0 | |
| fi | |
| git commit -m "[tests] Update expected app size files | |
| Applied from gist: $GIST_URL" | |
| git push | |
| - name: Post success comment | |
| if: success() | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| GIST_URL: ${{ steps.parse.outputs.gist_url }} | |
| with: | |
| script: | | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body: `✅ Applied expected app size files from [gist](${process.env.GIST_URL}).`, | |
| }); | |
| - name: Post failure comment | |
| if: failure() | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body: `❌ Failed to apply gist. Check the [workflow run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}) for details.`, | |
| }); |