Skip to content

Instantly share code, notes, and snippets.

@dscho
Last active October 11, 2021 12:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save dscho/585172971f0989ed340a563bbacc6cad to your computer and use it in GitHub Desktop.
Save dscho/585172971f0989ed340a563bbacc6cad to your computer and use it in GitHub Desktop.
Trigger GitHub workflow_dispatch from the blob view of the workflow file
// ==UserScript==
// @name Trigger workflow_dispatch from the blob view of the workflow file
// @source https://gist.github.com/dscho/585172971f0989ed340a563bbacc6cad
// @updateURL https://gist.github.com/dscho/585172971f0989ed340a563bbacc6cad/raw/workflow_dispatch-from-blob.user.js
// @downloadURL https://gist.github.com/dscho/585172971f0989ed340a563bbacc6cad/raw/workflow_dispatch-from-blob.user.js
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Trigger a workflow from the blob view of the workflow file
// @author Johannes Schindelin
// @match https://github.com/*/blob/*/.github/workflows/*.yml
// @icon https://www.google.com/s2/favicons?domain=github.com
// @grant GM_addElement
// @grant GM.xmlHttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @connect raw.githubusercontent.com
// @connect api.github.com
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.5.0/jquery.min.js
// @require https://raw.githubusercontent.com/nodeca/js-yaml/master/dist/js-yaml.min.js
// ==/UserScript==
(function() {
'use strict';
const match = window.location.toString().match(/^(https:\/\/github\.com\/([^/]+\/[^/]+)\/)blob(\/(.*)\/\.github\/workflows\/([^/]+\.yml))$/)
if (!match) return;
const ownerRepo = match[2];
const ref = match[4];
const filename = match[5];
const addButton = (innerHTML, callback) => {
const button = GM_addElement(document.getElementById('blob-path').parentElement, 'a', {
class: "btn mr-2 d-none d-md-block"
});
button.innerHTML = innerHTML;
button.onclick = callback;
return button;
};
const handleYAML = (yaml) => {
const parsed = jsyaml.load(yaml);
document.myParsed = parsed;
if (parsed?.on?.workflow_dispatch === undefined) return;
let button;
let formDiv;
button = addButton("Run workflow", () => {
button.onclick = undefined;
const div = GM_addElement(button, 'div', {
id: "workflow-popover",
class: "Popover position-relative position-absolute Popover-message Popover-message--large Popover-message--top-right mt-2 right-0 text-left p-3 mx-auto Box color-shadow-large"
});
button.onclick = (e) => {
if (e.target !== button) return;
e.stopPropagation();
div.style.display = (div.style.display === 'none' ? '' : 'none');
};
for (const [label, input] of Object.entries(parsed.on.workflow_dispatch?.inputs || {})) {
const group = GM_addElement(div, 'div', {
class: "form-group mt-1 mb-2"
});
const groupHeader = GM_addElement(group, 'div', {
class: "form-group-header"
});
GM_addElement(groupHeader, 'label', {
class: "color-text-primary text-mono f6"
}).innerHTML = input.description || label;
const groupBody = GM_addElement(group, 'div', {
class: "form-group-body"
});
const inputElement = GM_addElement(groupBody, 'input', {
class: "form-control input-contrast input-sm",
type: "text",
name: `inputs[${label}]`,
value: input.default
});
input.inputElement = inputElement;
}
let pat
let token = GM_getValue('gh-token');
if (token === undefined) {
GM_addElement(div, 'label', {
class: "color-text-primary text-mono f6"
}).innerHTML = "PAT";
pat = GM_addElement(div, 'input', {
class: "form-control input-contrast input-sm",
type: "password",
name: "PAT"
});
}
const runButton = GM_addElement(div, 'button', {
type: "submit",
class: "btn btn-primary btn-sm mt-2",
autofocus: ""
});
runButton.innerHTML = "Run workflow";
const onclick = () => {
runButton.onclick = undefined;
if (token === undefined) {
if (!pat.value) {
runButton.onclick = onclick;
pat.focus();
return;
}
pat.style.display = 'none';
pat.previousSibling.style.display = 'none';
token = pat.value;
pat.value = '';
GM_setValue('gh-token', token);
}
runButton.innerHTML = "<i>Starting</i>";
const triggerURL = `https://api.github.com/repos/${ownerRepo}/actions/workflows/${filename}/dispatches`;
const triggerBody = { ref: ref, inputs: {} };
for (const [label, input] of Object.entries(parsed.on.workflow_dispatch?.inputs || {})) {
triggerBody.inputs[label] = input.inputElement.value;
}
GM.xmlHttpRequest({
url: triggerURL,
method: 'POST',
data: JSON.stringify(triggerBody),
headers: {
accept: "application/vnd.github.v3+json"
},
headers: {
Authorization: `token ${token}`
},
onload: (data) => {
div.style.display = 'none';
const workflowsURL = `https://github.com/${ownerRepo}/actions/workflows/${filename}?query=branch%3A${encodeURIComponent(ref)}`;
window.location = workflowsURL;
},
onerror: (data) => {
console.log(data);
alert("Error!");
runButton.onclick = onclick;
runButton.innerHTML = "Run workflow";
}
});
};
runButton.onclick = onclick;
});
};
const rawURL = `https://raw.githubusercontent.com/${ownerRepo}/${match[3]}`;
// console.log(`${match[1]}raw${match[3]}`);
GM.xmlHttpRequest({
url: rawURL,
headers: {
accept: "text/plain",
},
onload: (data) => {
if (data.status === 200)
handleYAML(data.response);
else if (data.status === 404) {
const token = GM_getValue('gh-token');
if (!token)
console.log("Private repository? Have no token to play with...");
else {
console.log("Private repository? Try again with token...");
GM.xmlHttpRequest({
url: rawURL,
headers: {
Accept: "text/plain",
Authorization: `token ${token}`
},
onload: (data) => {
if (data.status === 200)
handleYAML(data.response);
else {
console.log(`Failed to retrieve ${rawURL} from private repository`);
console.log(data);
}
},
onerror: (data) => {
console.log(`Failed to retrieve ${rawURL}`);
console.log(data);
}
});
}
return;
} else {
console.log(`Unhandled status ${data.status} when retrieving ${rawURL}`);
console.log(data);
}
},
onerror: (data) => {
console.log(`Failed to retrieve ${rawURL}`);
console.log(data);
}
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment