Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions .github/workflows/open-upstream-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Automatically opens a PR to the upstream repository material-components/material-web
# when a PR is merged into main-community in this fork.
# Requirements: (recommended) secret UPSTREAM_TOKEN with public_repo (or repo) scope.
# The triggering PR's head branch MUST be based off 'main' (not another feature branch) or it will be skipped.

name: Mirror merged PR upstream

on:
pull_request:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although I do worry about the possibility of just spamming the main repo. I wonder if we shouldn't have a separate branch called upstream or something like that, because let's say if we added a new component through one PR but then realized there is something that needs to be fixed and created and merged a new PR for that, it would create 2 PRs where only one PR would possibly be necessary.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but with another branch we would have one PR with multiple components with one upstream branch

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that as Material Web community grows, it will make review difficult if we continuously update a single large PR

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't mean for there to be one large PR. I think the main issue is that the PRs are just going to pile up because I don't have any illusions that they would potentially accept new stuff, etc, fixes maybe.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true. Maybe we can do one branch per component (i.e. feat/components/<component) and then create a PR only for that (and we'll update that branch in the time). The workflow supports the no-upstream label to skip PRs

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can do one branch per component (i.e. feat/components/<component) and then create a PR only for that (and we'll update that branch in the time).

Yep I do agree with that I think it is gonna be probably for the best

types: [closed]

concurrency:
group: mirror-upstream-${{ github.event.pull_request.number }}
cancel-in-progress: false

permissions:
contents: write
pull-requests: write

jobs:
create-upstream-pr:
if: >-
${{ github.event.pull_request.merged == true &&
github.event.pull_request.base.ref == 'main-community' &&
github.event.pull_request.head.repo.full_name == github.repository &&
github.repository != 'material-components/material-web' }}
runs-on: ubuntu-latest
env:
UPSTREAM_REPO: material-components/material-web
SKIP_LABEL: no-upstream
BASE_TARGET: main
REQUIRED_ANCESTOR: main
# Fallback: if UPSTREAM_TOKEN is set and not empty use it, otherwise default to GITHUB_TOKEN (may lack upstream permissions)
TOKEN: ${{ secrets.UPSTREAM_TOKEN != '' && secrets.UPSTREAM_TOKEN || secrets.GITHUB_TOKEN }}
steps:
- name: Check skip label
id: check_label
run: |
labels='${{ toJson(github.event.pull_request.labels) }}'
echo "Labels: $labels"
if echo "$labels" | grep -q '"name":"'"$SKIP_LABEL"'"'; then
Copy link

Copilot AI Sep 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The nested quoting pattern is complex and error-prone. Consider using jq to parse the labels JSON more reliably: if echo '$labels' | jq -e '.[] | select(.name == env.SKIP_LABEL)' >/dev/null 2>&1; then

Suggested change
if echo "$labels" | grep -q '"name":"'"$SKIP_LABEL"'"'; then
if echo "$labels" | jq -e '.[] | select(.name == env.SKIP_LABEL)' >/dev/null 2>&1; then

Copilot uses AI. Check for mistakes.
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Stop if skip label present
if: steps.check_label.outputs.skip == 'true'
run: echo "Skip label present, abort upstream mirroring" && exit 0

- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set branch variables
id: vars
run: |
echo "branch=${{ github.event.pull_request.head.ref }}" >> $GITHUB_OUTPUT
echo "head_sha=${{ github.event.pull_request.head.sha }}" >> $GITHUB_OUTPUT
echo "author_login=${{ github.event.pull_request.user.login }}" >> $GITHUB_OUTPUT
echo "repo_full=${{ github.repository }}" >> $GITHUB_OUTPUT

- name: Verify branch is based on 'main'
id: verify_base
run: |
BRANCH='${{ steps.vars.outputs.branch }}'
echo "Verifying branch $BRANCH is based on REQUIRED_ANCESTOR=${REQUIRED_ANCESTOR}"
git fetch origin ${REQUIRED_ANCESTOR} --quiet
git fetch origin $BRANCH --quiet || true
# Ensure required ancestor is an ancestor of head SHA
if git merge-base --is-ancestor origin/${REQUIRED_ANCESTOR} ${{ steps.vars.outputs.head_sha }}; then
echo "Base verification passed"
echo "ok=true" >> $GITHUB_OUTPUT
else
echo "Branch is NOT based on origin/${REQUIRED_ANCESTOR}; skipping upstream PR";
echo "ok=false" >> $GITHUB_OUTPUT
fi
- name: Stop if not based on main
if: steps.verify_base.outputs.ok != 'true'
run: echo "Branch not based on 'main'. Aborting." && exit 0

- name: Create (or skip) upstream PR
id: create_upstream_pr
env:
BRANCH: ${{ steps.vars.outputs.branch }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO_OWNER: ${{ github.repository_owner }}
REPO_FULL: ${{ steps.vars.outputs.repo_full }}
AUTHOR: ${{ steps.vars.outputs.author_login }}
UPSTREAM_REPO: ${{ env.UPSTREAM_REPO }}
BASE_TARGET: ${{ env.BASE_TARGET }}
TOKEN: ${{ env.TOKEN }}
run: |
set -euo pipefail
echo "Checking for existing open PR to $UPSTREAM_REPO with head $REPO_OWNER:$BRANCH"

export GH_TOKEN="$TOKEN"
existing=$(gh pr list -R "$UPSTREAM_REPO" --state open --head "$REPO_OWNER:$BRANCH" --json number --jq '.[0].number')
Copy link

Copilot AI Sep 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The jq expression .[0].number will fail with 'null cannot be indexed with number' when the array is empty. Use .[0].number // empty or check array length first to handle cases where no PRs exist.

Copilot uses AI. Check for mistakes.
if [ -n "$existing" ]; then
echo "An upstream PR already exists (#$existing). Exiting."; exit 0; fi

body_content=${PR_BODY:-"(No description provided in the source PR)"}
body_content+="\n\n(Automated upstream mirror of source PR ${REPO_FULL}#${PR_NUMBER} by @${AUTHOR})"
Copy link

Copilot AI Sep 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shell string concatenation with += may not work as expected with newlines. Use body_content=\"${body_content}\n\n(Automated upstream mirror of source PR ${REPO_FULL}#${PR_NUMBER} by @${AUTHOR})\" instead.

Suggested change
body_content+="\n\n(Automated upstream mirror of source PR ${REPO_FULL}#${PR_NUMBER} by @${AUTHOR})"
body_content="${body_content}\n\n(Automated upstream mirror of source PR ${REPO_FULL}#${PR_NUMBER} by @${AUTHOR})"

Copilot uses AI. Check for mistakes.
printf '%s\n' "$body_content" > body.md

echo "Creating PR on $UPSTREAM_REPO: base=$BASE_TARGET head=$REPO_OWNER:$BRANCH"
gh pr create -R "$UPSTREAM_REPO" \
--title "$PR_TITLE" \
--body-file body.md \
--base "$BASE_TARGET" \
--head "$REPO_OWNER:$BRANCH"

new_number=$(gh pr list -R "$UPSTREAM_REPO" --state open --head "$REPO_OWNER:$BRANCH" --json number --jq '.[0].number') || true
Copy link

Copilot AI Sep 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The jq expression .[0].number will fail with 'null cannot be indexed with number' when the array is empty. Use .[0].number // empty or check array length first to handle cases where no PRs exist.

Suggested change
new_number=$(gh pr list -R "$UPSTREAM_REPO" --state open --head "$REPO_OWNER:$BRANCH" --json number --jq '.[0].number') || true
new_number=$(gh pr list -R "$UPSTREAM_REPO" --state open --head "$REPO_OWNER:$BRANCH" --json number --jq '.[0].number // empty') || true

Copilot uses AI. Check for mistakes.
if [ -n "${new_number}" ]; then
upstream_url=$(gh pr view -R "$UPSTREAM_REPO" "${new_number}" --json url --jq '.url') || upstream_url=""
echo "Upstream PR created successfully (#${new_number}).";
echo "new_number=${new_number}" >> $GITHUB_OUTPUT
if [ -n "$upstream_url" ]; then
echo "upstream_url=${upstream_url}" >> $GITHUB_OUTPUT
fi
else
echo "Upstream PR creation command finished but PR number could not be determined.";
fi

- name: Comment back on source PR with upstream link
if: ${{ steps.create_upstream_pr.outputs.new_number != '' }}
env:
GH_TOKEN: ${{ env.TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
UPSTREAM_REPO: ${{ env.UPSTREAM_REPO }}
UPSTREAM_PR_NUMBER: ${{ steps.create_upstream_pr.outputs.new_number }}
UPSTREAM_URL: ${{ steps.create_upstream_pr.outputs.upstream_url }}
run: |
set -euo pipefail
url_display=${UPSTREAM_URL:-"https://github.com/${UPSTREAM_REPO}/pull/${UPSTREAM_PR_NUMBER}"}
comment="Upstream PR opened: ${UPSTREAM_REPO}#${UPSTREAM_PR_NUMBER}\n${url_display}\n\n(This comment was added automatically)"
echo "Posting comment to source PR #${PR_NUMBER}:"; echo "$comment"
gh pr comment "$PR_NUMBER" --body "$comment"