Replace index with id in actions routes (#36842)

This PR migrates the web Actions run/job routes from index-based
`runIndex` or `jobIndex` to database IDs.

**⚠️ BREAKING ⚠️**: Existing saved links/bookmarks that use the old
index-based URLs will no longer resolve after this change.

Improvements of this change:
- Previously, `jobIndex` depended on list order, making it hard to
locate a specific job. Using `jobID` provides stable addressing.
- Web routes now align with API, which already use IDs.
- Behavior is closer to GitHub, which exposes run/job IDs in URLs.
- Provides a cleaner base for future features without relying on list
order.
- #36388 this PR improves the support for reusable workflows. If a job
uses a reusable workflow, it may contain multiple child jobs, which
makes relying on job index to locate a job much more complicated

---------

Signed-off-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Zettat123
2026-03-10 15:14:48 -06:00
committed by GitHub
parent 6e8f78ae27
commit 385994295d
33 changed files with 713 additions and 228 deletions

View File

@@ -110,8 +110,8 @@ export default defineComponent({
WorkflowGraph,
},
props: {
runIndex: {type: Number, required: true},
jobIndex: {type: Number, required: true},
runId: {type: Number, required: true},
jobId: {type: Number, required: true},
actionsURL: {type: String, required: true},
locale: {
type: Object as PropType<Record<string, any>>,
@@ -366,7 +366,7 @@ export default defineComponent({
// for example: make cursor=null means the first time to fetch logs, cursor=eof means no more logs, etc
return {step: idx, cursor: it.cursor, expanded: it.expanded};
});
const resp = await POST(`${this.actionsURL}/runs/${this.runIndex}/jobs/${this.jobIndex}`, {
const resp = await POST(`${this.actionsURL}/runs/${this.runId}/jobs/${this.jobId}`, {
signal: abortController.signal,
data: {logCursors},
});
@@ -538,13 +538,13 @@ export default defineComponent({
<div class="action-view-left">
<div class="job-group-section">
<div class="job-brief-list">
<a class="job-brief-item" :href="run.link+'/jobs/'+index" :class="jobIndex === index ? 'selected' : ''" v-for="(job, index) in run.jobs" :key="job.id">
<a class="job-brief-item" :href="run.link+'/jobs/'+job.id" :class="jobId === job.id ? 'selected' : ''" v-for="job in run.jobs" :key="job.id">
<div class="job-brief-item-left">
<ActionRunStatus :locale-status="locale.status[job.status]" :status="job.status"/>
<span class="job-brief-name tw-mx-2 gt-ellipsis">{{ job.name }}</span>
</div>
<span class="job-brief-item-right">
<SvgIcon name="octicon-sync" role="button" :data-tooltip-content="locale.rerun" class="job-brief-rerun tw-mx-2 link-action interact-fg" :data-url="`${run.link}/jobs/${index}/rerun`" v-if="job.canRerun"/>
<SvgIcon name="octicon-sync" role="button" :data-tooltip-content="locale.rerun" class="job-brief-rerun tw-mx-2 link-action interact-fg" :data-url="`${run.link}/jobs/${job.id}/rerun`" v-if="job.canRerun"/>
<span class="step-summary-duration">{{ job.duration }}</span>
</span>
</a>
@@ -581,7 +581,7 @@ export default defineComponent({
<WorkflowGraph
v-if="showWorkflowGraph && run.jobs.length > 1"
:jobs="run.jobs"
:current-job-index="jobIndex"
:current-job-id="jobId"
:run-link="run.link"
:workflow-id="run.workflowID"
class="workflow-graph-container"
@@ -626,7 +626,7 @@ export default defineComponent({
</a>
<div class="divider"/>
<a :class="['item', !currentJob.steps.length ? 'disabled' : '']" :href="run.link+'/jobs/'+jobIndex+'/logs'" download>
<a :class="['item', !currentJob.steps.length ? 'disabled' : '']" :href="run.link+'/jobs/'+jobId+'/logs'" download>
<i class="icon"><SvgIcon name="octicon-download"/></i>
{{ locale.downloadLogs }}
</a>

View File

@@ -40,7 +40,7 @@ interface StoredState {
const props = defineProps<{
jobs: ActionsJob[];
currentJobIndex: number;
currentJobId: number;
runLink: string;
workflowId: string;
}>()
@@ -588,9 +588,9 @@ function computeJobLevels(jobs: ActionsJob[]): Map<string, number> {
}
function onNodeClick(job: JobNode, event: MouseEvent) {
if (job.index === props.currentJobIndex) return;
if (job.id === props.currentJobId) return;
const link = `${props.runLink}/jobs/${job.index}`;
const link = `${props.runLink}/jobs/${job.id}`;
if (event.ctrlKey || event.metaKey) {
window.open(link, '_blank');
return;
@@ -652,7 +652,7 @@ function onNodeClick(job: JobNode, event: MouseEvent) {
<g
v-for="job in jobsWithLayout"
:key="job.id"
:class="{'current-job': job.index === currentJobIndex}"
:class="{'current-job': job.id === currentJobId}"
class="job-node-group"
@click="onNodeClick(job, $event)"
@mouseenter="handleNodeMouseEnter(job)"
@@ -665,8 +665,8 @@ function onNodeClick(job: JobNode, event: MouseEvent) {
:height="nodeHeight"
rx="8"
:fill="getNodeColor(job.status)"
:stroke="job.index === currentJobIndex ? 'var(--color-primary)' : 'var(--color-card-border)'"
:stroke-width="job.index === currentJobIndex ? '3' : '2'"
:stroke="job.id === currentJobId ? 'var(--color-primary)' : 'var(--color-card-border)'"
:stroke-width="job.id === currentJobId ? '3' : '2'"
class="job-rect"
/>

View File

@@ -11,8 +11,8 @@ export function initRepositoryActionView() {
if (parentFullHeight) parentFullHeight.classList.add('tw-pb-0');
const view = createApp(RepoActionView, {
runIndex: parseInt(el.getAttribute('data-run-index')!),
jobIndex: parseInt(el.getAttribute('data-job-index')!),
runId: parseInt(el.getAttribute('data-run-id')!),
jobId: parseInt(el.getAttribute('data-job-id')!),
actionsURL: el.getAttribute('data-actions-url'),
locale: {
approve: el.getAttribute('data-locale-approve'),