fix: incorrect identification of outdated run attempts (#12021)

Since https://codeberg.org/forgejo/forgejo/pulls/11750, the attempt number of a Forgejo Actions job is set eagerly. When an job is ultimately not run, for example, because its `needs` weren't satisfied, it leads to discontinuous attempt numbers of completed attempts that the component for viewing action logs could not handle. This has been rectified by actually determining the number of the last attempt.

Resolves https://codeberg.org/forgejo/forgejo/issues/11994.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12021
Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org>
Co-authored-by: Andreas Ahlenstorf <andreas@ahlenstorf.ch>
Co-committed-by: Andreas Ahlenstorf <andreas@ahlenstorf.ch>
This commit is contained in:
Andreas Ahlenstorf 2026-04-08 20:03:10 +02:00 committed by Gusted
commit 4b2969ab84
2 changed files with 19 additions and 16 deletions

View file

@ -183,7 +183,8 @@ function configureForMultipleAttemptTests({viewHistorical}) {
},
],
allAttempts: [
{number: 2, time_since_started_html: 'yesterday', status: 'success', status_diagnostics: ['Success']},
{number: 3, time_since_started_html: 'yesterday', status: 'success', status_diagnostics: ['Success']},
// Omit one attempt to simulate the case when a job isn't run because a `needs:` failed.
{number: 1, time_since_started_html: 'two days ago', status: 'failure', status_diagnostics: ['Failure']},
],
},
@ -218,7 +219,7 @@ function configureForMultipleAttemptTests({viewHistorical}) {
props: {
...defaultTestProps,
runIndex: '123',
attemptNumber: viewHistorical ? '1' : '2',
attemptNumber: viewHistorical ? '1' : '3',
actionsURL: toAbsoluteUrl('/user1/repo2/actions'),
initialJobData: {...minimalInitialJobData, state: myJobState},
},
@ -243,7 +244,7 @@ test('display baseline with most-recent attempt', async () => {
// Attempt selector dropdown...
expect(wrapper.findAll('.job-attempt-dropdown').length).toEqual(1);
expect(wrapper.findAll('.job-attempt-dropdown .svg.octicon-check-circle-fill.text.green').length).toEqual(1);
expect(wrapper.get('.job-attempt-dropdown .ui.dropdown').text()).toEqual('Run attempt 2 yesterday');
expect(wrapper.get('.job-attempt-dropdown .ui.dropdown').text()).toEqual('Run attempt 3 yesterday');
// Attempt status
expect(wrapper.get('.job-info-header h3').text()).toEqual('test');
@ -302,7 +303,7 @@ test('historical attempt dropdown interactions', async () => {
const attemptsExpanded = () => {
expect(wrapper.findAll('.job-attempt-dropdown .action-job-menu').length).toEqual(1);
expect(wrapper.get('.job-attempt-dropdown .action-job-menu').isVisible()).toBe(true);
expect(wrapper.findAll('.job-attempt-dropdown .action-job-menu a').filter((a) => a.text() === 'Run attempt 2 yesterday').length).toEqual(1);
expect(wrapper.findAll('.job-attempt-dropdown .action-job-menu a').filter((a) => a.text() === 'Run attempt 3 yesterday').length).toEqual(1);
expect(wrapper.findAll('.job-attempt-dropdown .action-job-menu a').filter((a) => a.text() === 'Run attempt 1 two days ago').length).toEqual(1);
};
attemptsExpanded();
@ -332,8 +333,8 @@ test('historical attempt dropdown interactions', async () => {
attemptsExpanded();
// Click on the other option in the dropdown to verify it navigates to the target attempt
wrapper.findAll('.job-attempt-dropdown .action-job-menu a').find((a) => a.text() === 'Run attempt 2 yesterday').trigger('click');
expect(window.location.href).toEqual(toAbsoluteUrl('/user1/repo2/actions/runs/123/jobs/1/attempt/2'));
wrapper.findAll('.job-attempt-dropdown .action-job-menu a').find((a) => a.text() === 'Run attempt 3 yesterday').trigger('click');
expect(window.location.href).toEqual(toAbsoluteUrl('/user1/repo2/actions/runs/123/jobs/1/attempt/3'));
});
test('run approval interaction', async () => {

View file

@ -124,10 +124,10 @@ export default {
],
// All available attempts for the job we're currently viewing.
//
// initial value here is configured so that currentingViewingMostRecentAttempt() -> true on the default `data()`, so that the
// initial value here is configured so that currentlyViewingMostRecentAttempt() -> true on the default `data()`, so that the
// initial render (before `loadJob`'s first execution is complete) doesn't display "You are viewing an
// out-of-date run..."
allAttempts: new Array(parseInt(this.attemptNumber)).fill({index: 0, time_since_started_html: '', status: 'success', status_diagnostics: []}),
allAttempts: [],
},
};
},
@ -138,19 +138,19 @@ export default {
},
displayOtherJobs() {
return this.currentingViewingMostRecentAttempt;
return this.currentlyViewingMostRecentAttempt;
},
canApprove() {
return this.currentingViewingMostRecentAttempt && this.run.canApprove;
return this.currentlyViewingMostRecentAttempt && this.run.canApprove;
},
canCancel() {
return this.currentingViewingMostRecentAttempt && this.run.canCancel;
return this.currentlyViewingMostRecentAttempt && this.run.canCancel;
},
canRerun() {
return this.currentingViewingMostRecentAttempt && this.run.canRerun;
return this.currentlyViewingMostRecentAttempt && this.run.canRerun;
},
viewingAttemptNumber() {
@ -167,11 +167,13 @@ export default {
return attempt || fallback;
},
currentingViewingMostRecentAttempt() {
if (!this.currentJob.allAttempts) {
currentlyViewingMostRecentAttempt() {
if (!this.currentJob.allAttempts || this.currentJob.allAttempts.length === 0) {
return true;
}
return this.viewingAttemptNumber === this.currentJob.allAttempts.length;
const mostRecentAttemptNumber = this.currentJob.allAttempts[0].number;
return this.viewingAttemptNumber === mostRecentAttemptNumber;
},
displayGearDropdown() {
@ -452,7 +454,7 @@ export default {
</script>
<template>
<div class="ui container fluid padded action-view-container" :class="{ 'interval-pending': intervalID }">
<div class="action-view-header job-out-of-date-warning" v-if="!currentingViewingMostRecentAttempt">
<div class="action-view-header job-out-of-date-warning" v-if="!currentlyViewingMostRecentAttempt">
<div class="ui warning message">
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-html="viewingOutOfDateRunLabel"/>