@@ -4,7 +4,9 @@ description: >
44 and never runs the agent's code. Applies the patch to the checkout, rejects one that changes
55 anything under .github/, commits it as the Playwright automation app with the title and body from
66 the metadata file, pushes it to the branch and, when asked, opens the branch as a draft pull
7- request with the body file as its description.
7+ request with the body file as its description. With a commit to rebase on, the patch is the whole
8+ branch against that commit, and the push replaces the branch when it is still at the checked-out
9+ commit.
810inputs :
911 token :
1012 description : ' Token with write access to contents and pull requests, from the app'
@@ -19,8 +21,12 @@ inputs:
1921 description : ' File name of the JSON in dir with the commit title and, optionally, body'
2022 required : true
2123 branch :
22- description : ' Branch to push to; created when it does not exist, never force-pushed '
24+ description : ' Branch to push to; created when it does not exist, replaced only with onto '
2325 required : true
26+ onto :
27+ description : ' Commit the branch was rebased on; the patch is then the whole branch against it'
28+ required : false
29+ default : ' '
2430 commit-body :
2531 description : ' Commit body when the metadata has none, such as the Fixes: line'
2632 required : false
@@ -67,33 +73,60 @@ runs:
6773 BASE : ${{ inputs.base }}
6874 BODY_FILE : ${{ inputs.body-file }}
6975 WORKFLOW_NAME : ${{ inputs.workflow-name }}
76+ ONTO : ${{ inputs.onto }}
7077 RUN_URL : ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
7178 with :
7279 github-token : ${{ inputs.token }}
7380 script : |
7481 const fs = require('fs');
7582 const path = require('path');
7683 const { execFileSync } = require('child_process');
77- const { DIR, PATCH, METADATA, BRANCH, COMMIT_BODY, PULL_REQUEST, BASE, BODY_FILE, WORKFLOW_NAME, RUN_URL } = process.env;
84+ const { DIR, PATCH, METADATA, BRANCH, COMMIT_BODY, PULL_REQUEST, BASE, BODY_FILE, WORKFLOW_NAME, ONTO, RUN_URL } = process.env;
7885 const git = (...args) => execFileSync('git', args, { encoding: 'utf8' }).trim();
7986 const { owner, repo } = context.repo;
87+ const { title, body } = JSON.parse(fs.readFileSync(path.join(DIR, METADATA), 'utf8'));
88+ if (typeof title !== 'string' || !title.trim())
89+ throw new Error(`${METADATA} needs a title`);
90+ const commitBody = typeof body === 'string' && body.trim() ? body : COMMIT_BODY;
91+ git('config', 'user.name', 'microsoft-playwright-automation[bot]');
92+ git('config', 'user.email', '203992400+microsoft-playwright-automation[bot]@users.noreply.github.com');
93+
94+ // The checkout is the commit the agent started from. The push fails if the branch moved since.
95+ const head = git('rev-parse', 'HEAD');
96+ let parent = head;
97+ let replayed = false;
98+ const pushOptions = [];
99+ if (ONTO) {
100+ if (!/^[0-9a-f]{40}$/.test(ONTO))
101+ throw new Error(`Invalid commit to rebase on: ${ONTO}`);
102+ git('fetch', '--no-tags', '--depth=500', 'origin', ONTO, head);
103+ // The commits of the branch stay when they replay cleanly; otherwise the branch becomes one commit.
104+ try {
105+ git('merge-base', ONTO, head);
106+ git('rebase', '--quiet', ONTO, head);
107+ parent = git('rev-parse', 'HEAD');
108+ replayed = true;
109+ } catch (error) {
110+ try { git('rebase', '--abort'); } catch {}
111+ parent = ONTO;
112+ }
113+ git('checkout', '-q', '--detach', ONTO);
114+ pushOptions.push(`--force-with-lease=refs/heads/${BRANCH}:${head}`);
115+ }
80116 git('apply', '--index', path.join(DIR, PATCH));
81117
82118 // Workflow and action changes would run with this repo's secrets in PR CI.
83119 const workflowFiles = git('diff', '--cached', '--name-only', '--no-renames', '-z').split('\0').filter(file => file.startsWith('.github/'));
84120 if (workflowFiles.length)
85121 throw new Error(`Patch changes workflow files, treating it as compromised: ${workflowFiles.join(', ')}`);
86122
87- const { title, body } = JSON.parse(fs.readFileSync(path.join(DIR, METADATA), 'utf8'));
88- if (typeof title !== 'string' || !title.trim())
89- throw new Error(`${METADATA} needs a title`);
90- const commitBody = typeof body === 'string' && body.trim() ? body : COMMIT_BODY;
91- git('config', 'user.name', 'microsoft-playwright-automation[bot]');
92- git('config', 'user.email', '203992400+microsoft-playwright-automation[bot]@users.noreply.github.com');
93- git('commit', '-m', title, ...(commitBody ? ['-m', commitBody] : []));
94- // Not a force push: it fails if an existing branch moved after the agent started.
95- git('push', 'origin', `HEAD:refs/heads/${BRANCH}`);
96- const commit = git('rev-parse', 'HEAD');
123+ const tree = git('write-tree');
124+ const unchanged = tree === git('rev-parse', `${parent}^{tree}`);
125+ if (unchanged && !replayed)
126+ throw new Error('Patch changes nothing');
127+ // A rebase without a change adds no empty commit.
128+ const commit = unchanged ? parent : git('commit-tree', tree, '-p', parent, '-m', title, ...(commitBody ? ['-m', commitBody] : []));
129+ git('push', ...pushOptions, 'origin', `${commit}:refs/heads/${BRANCH}`);
97130 core.setOutput('commit', commit);
98131 if (PULL_REQUEST !== 'true') {
99132 await core.summary.addRaw(`Pushed https://github.com/${owner}/${repo}/commit/${commit}\n`).write();
0 commit comments