Check disabled workflow when rerun jobs (#26535)

In GitHub, we can not rerun jobs if the workflow is disabled.

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
yp05327 2023-08-22 11:30:02 +09:00 committed by GitHub
parent b3f7137174
commit a4a567f29f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 77 additions and 101 deletions

View file

@ -3503,6 +3503,7 @@ workflow.disable = Disable Workflow
workflow.disable_success = Workflow '%s' disabled successfully. workflow.disable_success = Workflow '%s' disabled successfully.
workflow.enable = Enable Workflow workflow.enable = Enable Workflow
workflow.enable_success = Workflow '%s' enabled successfully. workflow.enable_success = Workflow '%s' enabled successfully.
workflow.disabled = Workflow is disabled.
need_approval_desc = Need approval to run workflows for fork pull request. need_approval_desc = Need approval to run workflows for fork pull request.

View file

@ -259,31 +259,35 @@ func ViewPost(ctx *context_module.Context) {
ctx.JSON(http.StatusOK, resp) ctx.JSON(http.StatusOK, resp)
} }
func RerunOne(ctx *context_module.Context) { // Rerun will rerun jobs in the given run
// jobIndex = 0 means rerun all jobs
func Rerun(ctx *context_module.Context) {
runIndex := ctx.ParamsInt64("run") runIndex := ctx.ParamsInt64("run")
jobIndex := ctx.ParamsInt64("job") jobIndex := ctx.ParamsInt64("job")
job, _ := getRunJobs(ctx, runIndex, jobIndex) run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
if ctx.Written() { if err != nil {
return
}
if err := rerunJob(ctx, job); err != nil {
ctx.Error(http.StatusInternalServerError, err.Error()) ctx.Error(http.StatusInternalServerError, err.Error())
return return
} }
ctx.JSON(http.StatusOK, struct{}{}) // can not rerun job when workflow is disabled
} cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
cfg := cfgUnit.ActionsConfig()
if cfg.IsWorkflowDisabled(run.WorkflowID) {
ctx.JSONError(ctx.Locale.Tr("actions.workflow.disabled"))
return
}
func RerunAll(ctx *context_module.Context) { job, jobs := getRunJobs(ctx, runIndex, jobIndex)
runIndex := ctx.ParamsInt64("run")
_, jobs := getRunJobs(ctx, runIndex, 0)
if ctx.Written() { if ctx.Written() {
return return
} }
if jobIndex != 0 {
jobs = []*actions_model.ActionRunJob{job}
}
for _, j := range jobs { for _, j := range jobs {
if err := rerunJob(ctx, j); err != nil { if err := rerunJob(ctx, j); err != nil {
ctx.Error(http.StatusInternalServerError, err.Error()) ctx.Error(http.StatusInternalServerError, err.Error())

View file

@ -1211,14 +1211,14 @@ func registerRoutes(m *web.Route) {
m.Combo(""). m.Combo("").
Get(actions.View). Get(actions.View).
Post(web.Bind(actions.ViewRequest{}), actions.ViewPost) Post(web.Bind(actions.ViewRequest{}), actions.ViewPost)
m.Post("/rerun", reqRepoActionsWriter, actions.RerunOne) m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
m.Get("/logs", actions.Logs) m.Get("/logs", actions.Logs)
}) })
m.Post("/cancel", reqRepoActionsWriter, actions.Cancel) m.Post("/cancel", reqRepoActionsWriter, actions.Cancel)
m.Post("/approve", reqRepoActionsWriter, actions.Approve) m.Post("/approve", reqRepoActionsWriter, actions.Approve)
m.Post("/artifacts", actions.ArtifactsView) m.Post("/artifacts", actions.ArtifactsView)
m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView) m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView)
m.Post("/rerun", reqRepoActionsWriter, actions.RerunAll) m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
}) })
}, reqRepoActionsReader, actions.MustEnableActions) }, reqRepoActionsReader, actions.MustEnableActions)

View file

@ -4,7 +4,9 @@ If you are customizing Gitea, please do not change this file.
If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly. If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly.
*/}} */}}
<script> <script>
{{/* before our JS code gets loaded, use arrays to store errors, then the arrays will be switched to our error handler later */}}
window.addEventListener('error', function(e) {window._globalHandlerErrors=window._globalHandlerErrors||[]; window._globalHandlerErrors.push(e);}); window.addEventListener('error', function(e) {window._globalHandlerErrors=window._globalHandlerErrors||[]; window._globalHandlerErrors.push(e);});
window.addEventListener('unhandledrejection', function(e) {window._globalHandlerErrors=window._globalHandlerErrors||[]; window._globalHandlerErrors.push(e);});
window.config = { window.config = {
appUrl: '{{AppUrl}}', appUrl: '{{AppUrl}}',
appSubUrl: '{{AppSubUrl}}', appSubUrl: '{{AppSubUrl}}',

View file

@ -20,6 +20,10 @@ export function showGlobalErrorMessage(msg) {
* @param {ErrorEvent} e * @param {ErrorEvent} e
*/ */
function processWindowErrorEvent(e) { function processWindowErrorEvent(e) {
if (e.type === 'unhandledrejection') {
showGlobalErrorMessage(`JavaScript promise rejection: ${e.reason}. Open browser console to see more details.`);
return;
}
if (!e.error && e.lineno === 0 && e.colno === 0 && e.filename === '' && window.navigator.userAgent.includes('FxiOS/')) { if (!e.error && e.lineno === 0 && e.colno === 0 && e.filename === '' && window.navigator.userAgent.includes('FxiOS/')) {
// At the moment, Firefox (iOS) (10x) has an engine bug. See https://github.com/go-gitea/gitea/issues/20240 // At the moment, Firefox (iOS) (10x) has an engine bug. See https://github.com/go-gitea/gitea/issues/20240
// If a script inserts a newly created (and content changed) element into DOM, there will be a nonsense error event reporting: Script error: line 0, col 0. // If a script inserts a newly created (and content changed) element into DOM, there will be a nonsense error event reporting: Script error: line 0, col 0.
@ -30,6 +34,10 @@ function processWindowErrorEvent(e) {
} }
function initGlobalErrorHandler() { function initGlobalErrorHandler() {
if (window._globalHandlerErrors?._inited) {
showGlobalErrorMessage(`The global error handler has been initialized, do not initialize it again`);
return;
}
if (!window.config) { if (!window.config) {
showGlobalErrorMessage(`Gitea JavaScript code couldn't run correctly, please check your custom templates`); showGlobalErrorMessage(`Gitea JavaScript code couldn't run correctly, please check your custom templates`);
} }
@ -40,7 +48,7 @@ function initGlobalErrorHandler() {
processWindowErrorEvent(e); processWindowErrorEvent(e);
} }
// then, change _globalHandlerErrors to an object with push method, to process further error events directly // then, change _globalHandlerErrors to an object with push method, to process further error events directly
window._globalHandlerErrors = {'push': (e) => processWindowErrorEvent(e)}; window._globalHandlerErrors = {_inited: true, push: (e) => processWindowErrorEvent(e)};
} }
initGlobalErrorHandler(); initGlobalErrorHandler();

View file

@ -14,7 +14,7 @@
<button class="ui basic small compact button red" @click="cancelRun()" v-else-if="run.canCancel"> <button class="ui basic small compact button red" @click="cancelRun()" v-else-if="run.canCancel">
{{ locale.cancel }} {{ locale.cancel }}
</button> </button>
<button class="ui basic small compact button gt-mr-0" @click="rerun()" v-else-if="run.canRerun"> <button class="ui basic small compact button gt-mr-0 link-action" :data-url="`${run.link}/rerun`" v-else-if="run.canRerun">
{{ locale.rerun_all }} {{ locale.rerun_all }}
</button> </button>
</div> </div>
@ -38,7 +38,7 @@
<span class="job-brief-name gt-mx-3 gt-ellipsis">{{ job.name }}</span> <span class="job-brief-name gt-mx-3 gt-ellipsis">{{ job.name }}</span>
</div> </div>
<span class="job-brief-item-right"> <span class="job-brief-item-right">
<SvgIcon name="octicon-sync" role="button" :data-tooltip-content="locale.rerun" class="job-brief-rerun gt-mx-3" @click="rerunJob(index)" v-if="job.canRerun && onHoverRerunIndex === job.id"/> <SvgIcon name="octicon-sync" role="button" :data-tooltip-content="locale.rerun" class="job-brief-rerun gt-mx-3 link-action" :data-url="`${run.link}/jobs/${index}/rerun`" v-if="job.canRerun && onHoverRerunIndex === job.id"/>
<span class="step-summary-duration">{{ job.duration }}</span> <span class="step-summary-duration">{{ job.duration }}</span>
</span> </span>
</a> </a>
@ -264,17 +264,6 @@ const sfc = {
this.loadJob(); // try to load the data immediately instead of waiting for next timer interval this.loadJob(); // try to load the data immediately instead of waiting for next timer interval
} }
}, },
// rerun a job
async rerunJob(idx) {
const jobLink = `${this.run.link}/jobs/${idx}`;
await this.fetchPost(`${jobLink}/rerun`);
window.location.href = jobLink;
},
// rerun workflow
async rerun() {
await this.fetchPost(`${this.run.link}/rerun`);
window.location.href = this.run.link;
},
// cancel a run // cancel a run
cancelRun() { cancelRun() {
this.fetchPost(`${this.run.link}/cancel`); this.fetchPost(`${this.run.link}/cancel`);

View file

@ -8,7 +8,7 @@ import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.js';
import {svg} from '../svg.js'; import {svg} from '../svg.js';
import {hideElem, showElem, toggleElem} from '../utils/dom.js'; import {hideElem, showElem, toggleElem} from '../utils/dom.js';
import {htmlEscape} from 'escape-goat'; import {htmlEscape} from 'escape-goat';
import {createTippy, showTemporaryTooltip} from '../modules/tippy.js'; import {showTemporaryTooltip} from '../modules/tippy.js';
import {confirmModal} from './comp/ConfirmModal.js'; import {confirmModal} from './comp/ConfirmModal.js';
import {showErrorToast} from '../modules/toast.js'; import {showErrorToast} from '../modules/toast.js';
@ -64,9 +64,9 @@ export function initGlobalButtonClickOnEnter() {
}); });
} }
// doRedirect does real redirection to bypass the browser's limitations of "location" // fetchActionDoRedirect does real redirection to bypass the browser's limitations of "location"
// more details are in the backend's fetch-redirect handler // more details are in the backend's fetch-redirect handler
function doRedirect(redirect) { function fetchActionDoRedirect(redirect) {
const form = document.createElement('form'); const form = document.createElement('form');
const input = document.createElement('input'); const input = document.createElement('input');
form.method = 'post'; form.method = 'post';
@ -79,6 +79,33 @@ function doRedirect(redirect) {
form.submit(); form.submit();
} }
async function fetchActionDoRequest(actionElem, url, opt) {
try {
const resp = await fetch(url, opt);
if (resp.status === 200) {
let {redirect} = await resp.json();
redirect = redirect || actionElem.getAttribute('data-redirect');
actionElem.classList.remove('dirty'); // remove the areYouSure check before reloading
if (redirect) {
fetchActionDoRedirect(redirect);
} else {
window.location.reload();
}
} else if (resp.status >= 400 && resp.status < 500) {
const data = await resp.json();
// the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error"
// but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond.
await showErrorToast(data.errorMessage || `server error: ${resp.status}`);
} else {
await showErrorToast(`server error: ${resp.status}`);
}
} catch (e) {
console.error('error when doRequest', e);
actionElem.classList.remove('is-loading', 'small-loading-icon');
await showErrorToast(i18n.network_error);
}
}
async function formFetchAction(e) { async function formFetchAction(e) {
if (!e.target.classList.contains('form-fetch-action')) return; if (!e.target.classList.contains('form-fetch-action')) return;
@ -115,50 +142,7 @@ async function formFetchAction(e) {
reqOpt.body = formData; reqOpt.body = formData;
} }
let errorTippy; await fetchActionDoRequest(formEl, reqUrl, reqOpt);
const onError = (msg) => {
formEl.classList.remove('is-loading', 'small-loading-icon');
if (errorTippy) errorTippy.destroy();
// TODO: use a better toast UI instead of the tippy. If the form height is large, the tippy position is not good
errorTippy = createTippy(formEl, {
content: msg,
interactive: true,
showOnCreate: true,
hideOnClick: true,
role: 'alert',
theme: 'form-fetch-error',
trigger: 'manual',
arrow: false,
});
};
const doRequest = async () => {
try {
const resp = await fetch(reqUrl, reqOpt);
if (resp.status === 200) {
const {redirect} = await resp.json();
formEl.classList.remove('dirty'); // remove the areYouSure check before reloading
if (redirect) {
doRedirect(redirect);
} else {
window.location.reload();
}
} else if (resp.status >= 400 && resp.status < 500) {
const data = await resp.json();
// the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error"
// but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond.
onError(data.errorMessage || `server error: ${resp.status}`);
} else {
onError(`server error: ${resp.status}`);
}
} catch (e) {
console.error('error when doRequest', e);
onError(i18n.network_error);
}
};
// TODO: add "confirm" support like "link-action" in the future
await doRequest();
} }
export function initGlobalCommon() { export function initGlobalCommon() {
@ -209,6 +193,7 @@ export function initGlobalCommon() {
$('.tabular.menu .item').tab(); $('.tabular.menu .item').tab();
document.addEventListener('submit', formFetchAction); document.addEventListener('submit', formFetchAction);
document.addEventListener('click', linkAction);
} }
export function initGlobalDropzone() { export function initGlobalDropzone() {
@ -269,41 +254,29 @@ export function initGlobalDropzone() {
} }
async function linkAction(e) { async function linkAction(e) {
e.preventDefault();
// A "link-action" can post AJAX request to its "data-url" // A "link-action" can post AJAX request to its "data-url"
// Then the browser is redirected to: the "redirect" in response, or "data-redirect" attribute, or current URL by reloading. // Then the browser is redirected to: the "redirect" in response, or "data-redirect" attribute, or current URL by reloading.
// If the "link-action" has "data-modal-confirm" attribute, a confirm modal dialog will be shown before taking action. // If the "link-action" has "data-modal-confirm" attribute, a confirm modal dialog will be shown before taking action.
const el = e.target.closest('.link-action');
if (!el) return;
const $this = $(this); e.preventDefault();
const redirect = $this.attr('data-redirect'); const url = el.getAttribute('data-url');
const doRequest = async () => {
const doRequest = () => { el.disabled = true;
$this.prop('disabled', true); await fetchActionDoRequest(el, url, {method: 'POST', headers: {'X-Csrf-Token': csrfToken}});
$.post($this.attr('data-url'), { el.disabled = false;
_csrf: csrfToken
}).done((data) => {
if (data && data.redirect) {
window.location.href = data.redirect;
} else if (redirect) {
window.location.href = redirect;
} else {
window.location.reload();
}
}).always(() => {
$this.prop('disabled', false);
});
}; };
const modalConfirmContent = htmlEscape($this.attr('data-modal-confirm') || ''); const modalConfirmContent = htmlEscape(el.getAttribute('data-modal-confirm') || '');
if (!modalConfirmContent) { if (!modalConfirmContent) {
doRequest(); await doRequest();
return; return;
} }
const isRisky = $this.hasClass('red') || $this.hasClass('yellow') || $this.hasClass('orange') || $this.hasClass('negative'); const isRisky = el.classList.contains('red') || el.classList.contains('yellow') || el.classList.contains('orange') || el.classList.contains('negative');
if (await confirmModal({content: modalConfirmContent, buttonColor: isRisky ? 'orange' : 'green'})) { if (await confirmModal({content: modalConfirmContent, buttonColor: isRisky ? 'orange' : 'green'})) {
doRequest(); await doRequest();
} }
} }
@ -354,7 +327,6 @@ export function initGlobalLinkActions() {
// Helpers. // Helpers.
$('.delete-button').on('click', showDeletePopup); $('.delete-button').on('click', showDeletePopup);
$('.link-action').on('click', linkAction);
} }
function initGlobalShowModal() { function initGlobalShowModal() {