Support legacy run/job index-based URLs and refactor migration 326 (#37008)
Follow up #36842 Migration `326` can be prohibitively slow on large instances because it scans and rewrites all commit status target URLs generated by Gitea Actions in the database. This PR refactors migration `326` to perform a partial update instead of rewriting every legacy target URL. The reason for this partial rewrite is that **smaller legacy run/job indexes are the most likely to be ambiguous with run/job ID-based URLs** during runtime resolution, so this change prioritizes that subset while avoiding the cost of rewriting all legacy records. To preserve access to old links, this PR introduces `resolveCurrentRunForView` to handle both ID-based URLs and index-based URLs: - For job pages (`/actions/runs/{run}/jobs/{job}`), it first tries to confirm that the URL is ID-based. It does so by checking whether `{job}` can be treated as an existing job ID in the repository and whether that job belongs to `{run}`. If that match cannot be confirmed, it falls back to treating the URL as legacy `run index + job index`, resolves the corresponding run and job, and redirects to the correct ID-based URL. - When both ID-based and index-based interpretations are valid at the same time, the resolver **prefers the ID-based interpretation by default**. For example, if a repository contains one run-job pair (`run_id=3, run_index=2, job_id=4`), and also another run-job pair (`run_id=1100, run_index=3, job_id=1200, job_index=4`), then `/actions/runs/3/jobs/4` is ambiguous. In that case, the resolver treats it as the ID-based URL by default and shows the page for `run_id=3, job_id=4`. Users can still explicitly force the legacy index-based interpretation with `?by_index=1`, which would resolve the same URL to `/actions/runs/1100/jobs/1200`. - For run summary pages (`/actions/runs/{run}`), it uses a best-effort strategy: by default it first treats `{run}` as a run ID, and if no such run exists in the repository, it falls back to treating `{run}` as a legacy run index and redirects to the ID-based URL. Users can also explicitly force the legacy interpretation with `?by_index=1`. - This summary-page compatibility is best-effort, not a strict ambiguity check. For example, if a repository contains two runs: runA (`id=7, index=3`) and runB (`id=99, index=7`), then `/actions/runs/7` will resolve to runA by default, even though the old index-based URL originally referred to runB. The table below shows how valid legacy index-based target URLs are handled before and after migration `326`. Lower-range legacy URLs are rewritten to ID-based URLs, while higher-range legacy URLs remain unchanged in the database but are still handled correctly by `resolveCurrentRunForView` at runtime. | run_id | run_index | job_id | job_index | old target URL | updated by migration 326 | current target URL | can be resolved correctly | |---|---|---|---|---|---|---|---| | 3 | 2 | 4 | 1 | `/user2/repo2/actions/runs/2/jobs/1` | true | `/user2/repo2/actions/runs/3/jobs/4` | true | | 4 | 3 | 8 | 4 | `/user2/repo2/actions/runs/3/jobs/4` | true | `/user2/repo2/actions/runs/4/jobs/8` | true (without migration 326, this URL will resolve to run(`id=3`)) | | 80 | 20 | 170 | 0 | `/user2/repo2/actions/runs/20/jobs/0` | true | `/user2/repo2/actions/runs/80/jobs/170` | true | | 1500 | 900 | 1600 | 0 | `/user2/repo2/actions/runs/900/jobs/0` | false | `/user2/repo2/actions/runs/900/jobs/0` | true | | 2400 | 1500 | 2600 | 0 | `/user2/repo2/actions/runs/1500/jobs/0` | false | `/user2/repo2/actions/runs/1500/jobs/0` | true | | 2400 | 1500 | 2601 | 1 | `/user2/repo2/actions/runs/1500/jobs/1` | false | `/user2/repo2/actions/runs/1500/jobs/1` | true | For users who already ran the old migration `326`, this change has no functional impact. Their historical URLs are already stored in the ID-based form, and ID-based URLs continue to resolve correctly. For users who have not run the old migration `326`, only a subset of legacy target URLs will now be rewritten during upgrade. This avoids the extreme runtime cost of the previous full migration, while all remaining legacy target URLs continue to work through the web-layer compatibility logic. Many thanks to @wxiaoguang for the suggestions.
This commit is contained in:
@@ -68,9 +68,138 @@ func getCurrentRunByPathParam(ctx *context_module.Context) (run *actions_model.A
|
||||
return run
|
||||
}
|
||||
|
||||
// resolveCurrentRunForView resolves GET Actions page URLs and supports both ID-based and legacy index-based forms.
|
||||
//
|
||||
// By default, run summary pages (/actions/runs/{run}) use a best-effort ID-first fallback,
|
||||
// and job pages (/actions/runs/{run}/jobs/{job}) try to confirm an ID-based URL first and prefer the ID-based interpretation when both are valid.
|
||||
//
|
||||
// `by_id=1` param explicitly forces the ID-based path, and `by_index=1` explicitly forces the legacy index-based path.
|
||||
// If both are present, `by_id` takes precedence.
|
||||
func resolveCurrentRunForView(ctx *context_module.Context) *actions_model.ActionRun {
|
||||
// `by_id` explicitly requests ID-based resolution, so the request skips the legacy index-based disambiguation logic and resolves the run by ID directly.
|
||||
// It takes precedence over `by_index` when both query parameters are present.
|
||||
if ctx.PathParam("run") == "latest" || ctx.FormBool("by_id") {
|
||||
return getCurrentRunByPathParam(ctx)
|
||||
}
|
||||
|
||||
runNum := ctx.PathParamInt64("run")
|
||||
if runNum <= 0 {
|
||||
ctx.NotFound(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
byIndex := ctx.FormBool("by_index")
|
||||
|
||||
if ctx.PathParam("job") == "" {
|
||||
// The URL does not contain a {job} path parameter, so it cannot use the
|
||||
// job-specific rules to disambiguate ID-based URLs from legacy index-based URLs.
|
||||
// Because of that, this path is handled with a best-effort ID-first fallback by default.
|
||||
//
|
||||
// When the same repository contains:
|
||||
// - a run whose ID matches runNum, and
|
||||
// - a different run whose repo-scope index also matches runNum
|
||||
// this path prefers the ID match and may show a different run than the old legacy URL originally intended,
|
||||
// unless `by_index=1` explicitly forces the legacy index-based interpretation.
|
||||
|
||||
if !byIndex {
|
||||
runByID, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runNum)
|
||||
if err == nil {
|
||||
return runByID
|
||||
}
|
||||
if !errors.Is(err, util.ErrNotExist) {
|
||||
ctx.ServerError("GetRun:"+ctx.PathParam("run"), err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
runByIndex, err := actions_model.GetRunByRepoAndIndex(ctx, ctx.Repo.Repository.ID, runNum)
|
||||
if err == nil {
|
||||
ctx.Redirect(fmt.Sprintf("%s/actions/runs/%d", ctx.Repo.RepoLink, runByIndex.ID), http.StatusFound)
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(err, util.ErrNotExist) {
|
||||
ctx.ServerError("GetRunByRepoAndIndex", err)
|
||||
return nil
|
||||
}
|
||||
ctx.NotFound(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
jobNum := ctx.PathParamInt64("job")
|
||||
if jobNum < 0 {
|
||||
ctx.NotFound(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
// A job index should not be larger than MaxJobNumPerRun, so larger values can skip the legacy index-based path and be treated as job IDs directly.
|
||||
if !byIndex && jobNum >= actions_model.MaxJobNumPerRun {
|
||||
return getCurrentRunByPathParam(ctx)
|
||||
}
|
||||
|
||||
var runByID, runByIndex *actions_model.ActionRun
|
||||
var targetJobByIndex *actions_model.ActionRunJob
|
||||
|
||||
// Each run must have at least one job, so a valid job ID in the same run cannot be smaller than the run ID.
|
||||
if !byIndex && jobNum >= runNum {
|
||||
// Probe the repo-scoped job ID first and only accept it when the job exists and belongs to the same runNum.
|
||||
job, err := actions_model.GetRunJobByRepoAndID(ctx, ctx.Repo.Repository.ID, jobNum)
|
||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||
ctx.ServerError("GetRunJobByRepoAndID", err)
|
||||
return nil
|
||||
}
|
||||
if job != nil {
|
||||
if err := job.LoadRun(ctx); err != nil {
|
||||
ctx.ServerError("LoadRun", err)
|
||||
return nil
|
||||
}
|
||||
if job.Run.ID == runNum {
|
||||
runByID = job.Run
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to resolve the request as a legacy run-index/job-index URL.
|
||||
{
|
||||
run, err := actions_model.GetRunByRepoAndIndex(ctx, ctx.Repo.Repository.ID, runNum)
|
||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||
ctx.ServerError("GetRunByRepoAndIndex", err)
|
||||
return nil
|
||||
}
|
||||
if run != nil {
|
||||
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRunJobsByRunID", err)
|
||||
return nil
|
||||
}
|
||||
if jobNum < int64(len(jobs)) {
|
||||
runByIndex = run
|
||||
targetJobByIndex = jobs[jobNum]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if runByID == nil && runByIndex == nil {
|
||||
ctx.NotFound(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
if runByID != nil && runByIndex == nil {
|
||||
return runByID
|
||||
}
|
||||
|
||||
if runByID == nil && runByIndex != nil {
|
||||
ctx.Redirect(fmt.Sprintf("%s/actions/runs/%d/jobs/%d", ctx.Repo.RepoLink, runByIndex.ID, targetJobByIndex.ID), http.StatusFound)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reaching this point means both ID-based and legacy index-based interpretations are valid. Prefer the ID-based interpretation by default.
|
||||
// Use `by_index=1` query parameter to access the legacy index-based interpretation when necessary.
|
||||
return runByID
|
||||
}
|
||||
|
||||
func View(ctx *context_module.Context) {
|
||||
ctx.Data["PageIsActions"] = true
|
||||
run := getCurrentRunByPathParam(ctx)
|
||||
run := resolveCurrentRunForView(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user