Skip to content

[net11.0] Update dependencies from dotnet/dotnet #3505

[net11.0] Update dependencies from dotnet/dotnet

[net11.0] Update dependencies from dotnet/dotnet #3505

Workflow file for this run

# 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.`,
});