12.1 KiB · text History 876aa0b

CI gaps against GitHub / Forgejo Actions

Executed (2026-10-17/19): every plan below landed — artifacts and cache ff3668c, job token a9f5a09, runner context e9900fa, job graph 15e553d, cancellation and concurrency 6b5083e, checks b812854, filter patterns 91138c0, matrix 6b0b893, triggers 2cafbbf 9b97d32 ad37081 587faf4 8a80762 a9bbffe, fork pull requests 1b35c31 a5b815d — with follow-ups in 5e43417 (server-side ref moves get push events), eaf7cde (re-runs), dcb7d88 (runner job ownership, cache reserving job) and abee805 (stale-job reaper heartbeat). Still open: #23’s artifacts v3/v4, and the “No plan” rows. See PROGRESS.md slice 77. The table below is the audit as it stood, kept as a record.

Status: audit of 2026-10-17, on master at bfce1e7 (after the path-filter, event_name and pull_request work). Each gap was checked in the code; the evidence column names the file and function. What forgejo-runner expects was checked against its source (code.forgejo.org/forgejo/runner: internal/app/run/runner.go, workflow.go, internal/pkg/report/reporter.go) and Forgejo’s server (services/actions/context.go).

Severity, highest first:

  • security — another repository’s data, or credentials, are exposed.
  • silently wrong — the workflow runs and does the wrong thing, or reads empty values, with no error.
  • silent no-op — a documented feature that never happens, with no error.
  • loud — fails visibly; the author finds out.

Ranked

# Gap Severity Evidence Plan
1 Artifact endpoints trust the URL’s run id, not the token: any job can list, download and create artifacts in any repository’s runs security lib/git_gud_web/controllers/actions_artifact_controller.ex create/2, list/2, download/2 (ignores run_id); RunnerJwtAuth assigns :runner_run_id, unused workflow-artifacts-and-cache.md
2 Job token stays valid 24 h after the job ends (publish packages, read private code) security lib/git_gud/workflows/runner_jwt.ex verify/1 checks only signature and exp (ttl_seconds 86 400) workflow-job-token.md
3 Cache uploads: any job can append to/finalize another repo’s entry; chunks stored in arrival order (actions/cache uploads 4 at a time) → caches > 32 MB corrupt; O(n²) append security + silently wrong lib/git_gud_web/controllers/actions_cache_controller.ex upload_chunk/2, finalize/2; lib/git_gud/workflows/actions_cache.ex append/2 (read-concat-write, Content-Range ignored) workflow-artifacts-and-cache.md
4 Cancel doesn’t stop a running job: the runner’s next UpdateTask (result unspecified) flips it back to running and the reply never says cancelled silently wrong lib/git_gud_web/controllers/runner_controller.ex update_task/2, map_result/1; WorkflowJob.status_changeset/3 has no terminal guard; Workflows.cancel/1 workflow-cancellation-and-concurrency.md
5 secrets.GITHUB_TOKEN is empty (only user secrets are sent) silently wrong RunnerController.build_task/1 (GitGud.Secrets.resolve_for/1 only) workflow-runner-context.md
6 Workflow-level env: and defaults: never reach the runner; on: replaced by "push" (loses dispatch input defaults) silently wrong lib/git_gud/workflows/runner_payload.ex encode/1 workflow-runner-context.md
7 needs.<id>.outputs / .result always empty silently wrong RunnerController.build_needs/1 returns %{}; snapshot kept in Workflows.resolve_dependents/1 but never sent (emptied in ed9a7cd while TaskNeed field numbers were still swapped — fixed in c4d7080) workflow-job-graph.md
8 if: always() / failure() jobs are cancelled after a failure; a job skipped by its if: is recorded as success and its dependents run; job continue-on-error ignored silently wrong Workflows.resolve_dependents/1 (cancels on failed need), RunnerController.map_result(:RESULT_SKIPPED) → "success", WorkflowJob.satisfies_dependency?/1, Workflows.maybe_complete_run/1 workflow-job-graph.md
9 github.event is an empty object (sent as a string); run_id, run_attempt, workflow_ref, server_url, api_url, ids missing — so github.event.pull_request.number, head_commit.message, inputs.* are blank silently wrong RunnerController.build_context_struct/4 ("event" => run.trigger, all values string_value in wrap_as_struct/1); runner reads taskContext["event"].GetStructValue() workflow-runner-context.md
10 concurrency: (groups, cancel-in-progress) ignored — concurrent deploys silently wrong nothing in lib/ reads concurrency (grep) workflow-cancellation-and-concurrency.md
11 One commit status per workflow name: a push run and a pull_request run on the same head (or two workflows sharing a name:) overwrite each other; no per-job statuses; PR page doesn’t refresh when CI finishes silently wrong Workflows.update_run_commit_status/1 (context gitgud-actions/<name>, upsert on (repository_id, sha, context)); lib/git_gud_web/live/pr_live/show.ex no ci_topic subscription workflow-checks.md
12 branches:/tags: * crosses /; no ! negation there; ?/+ mean “any char”/literal instead of GitHub’s quantifiers (also in paths:) silently wrong Yaml.glob_match?/2, Yaml.kind_match?/5; lib/git_gud/workflows/glob.ex translate/2 workflow-filter-patterns.md
13 permissions: ignored: tokens are never narrowed (silent over-privilege), and contents: write can’t push (loud) silently wrong / loud RunnerJwt.mint/2 (no scope claim); GitGudWeb.Plugs.GitSmartHttp (job token: upload-pack only); GitGud.Packages.Access.allowed?/3 workflow-job-token.md
14 matrix: ${{ fromJSON(...) }} collapses to one leg with an empty matrix; max-parallel ignored; a fully-excluded matrix runs one empty leg silently wrong lib/git_gud/workflows/matrix.ex expand/1 (expand(_) -> [%{}]), pin_definition/2 (drops strategy / max-parallel); Workflows.do_claim_next_job/2 workflow-matrix.md
15 workflow_dispatch inputs dropped (API accepts, discards); no “Run workflow” button silently wrong lib/git_gud_web/controllers/api/v1/workflow_controller.ex (module doc), Workflows.dispatch_manual/4 workflow-triggers.md (B)
16 [skip ci] / skip-checks: true ignored silently wrong (extra runs) Workflows.dispatch/2, dispatch_pull_request/3 workflow-triggers.md (A)
17 on: schedule never fires silent no-op no scheduler reads workflow crons (config/config.exs Oban cron has only internal workers) workflow-triggers.md (C)
18 pull_request closed / edited / labeled / unlabeled never fire silent no-op Workflows.dispatch_pull_request/3 (refuses non-open), only opened/synchronize/reopened enqueued (PullRequests) workflow-triggers.md (E)
19 on: workflow_run never fires; the workflow_run webhook fires only for successful (or cancelled) runs, never failed ones silent no-op / silently wrong Workflows.maybe_complete_run/1 (calls notify_run_webhook/1 in the success branch only) workflow-triggers.md (F)
20 create / delete never fire silent no-op Repositories.Workers.CommitBackfill.perform/1 (returns before dispatch on delete; new refs only as push) workflow-triggers.md (D)
21 Fork and federated PRs never run (deliberate, debug log only); no pull_request_target silent no-op Workflows.dispatch_pull_request/3 workflow-fork-pull-requests.md
22 Branch protection can’t require status checks: require_status_checks / required_checks columns exist, are cast, and are read by nothing; not in the UI silent no-op lib/git_gud/branch_protections/branch_protection.ex; BranchProtections.evaluate/5; branch_protection_live/index.ex workflow-checks.md
23 actions/upload-artifact@v4 / download-artifact@v4 unsupported (only the v1 pipelines API exists); README claims “artifacts API v4” loud lib/git_gud_web/router.ex (/pipelines/workflows/:run_id/artifacts only) workflow-artifacts-and-cache.md fixes the README; v4 itself is listed there under “Out” as the next plan

No plan

Gap / feature Why no plan
on: release gitgud has no releases (lib/git_gud/release.ex is the Mix release module). Plan it with a releases feature.
environment: (environment secrets, protection rules, deployments) gitgud has no environments; the key passes through to act, which ignores it, and the job runs with repository secrets. Worth a plan only together with an environments feature.
OIDC (permissions: id-token: write, ACTIONS_ID_TOKEN_REQUEST_*) Loud (the action errors when the variables are missing); needs an OIDC issuer in gitgud — a feature, not a gap fix.
Reusable workflows (jobs.<id>.uses: ./.github/workflows/x.yml) and on: workflow_call Unverified. Workflows.create_job_leg/3 creates such a job with runs-on defaulted to ubuntu-latest and no steps, and hands its definition to the runner; whether forgejo-runner/act expands a local uses: there was not tested. Test on a real runner first; plan only if it fails.
Branch-protection patterns, whose * also crosses / (BranchProtections.matches_pattern?/2) Deliberately left: tightening it would silently unprotect nested branches. A maintainer decision, noted in the filter-patterns plan.
mix precommit can’t pass on master Not a workflow gap: ~25 pre-existing compile warnings fail --warnings-as-errors, and a repo-wide mix format rewrites ~40 files (non-idempotent HEEx output). Every plan here works around it (compare warning lists, format only touched files). Worth a maintenance pass of its own.

Works (checked, nothing to plan)

  • Step-level if:, continue-on-error, timeout-minutes, step outputs, job/step env, job defaults, services:, container:, job timeout-minutes — all live in the job’s definition, which is passed to act intact (RunnerPayload.encode/1, job.definition), and act implements them.
  • Job outputs: are collected (update_task/2 → Workflows.set_outputs/2); only their delivery to dependents is broken (#7).
  • runs-on label routing incl. self-hosted and matrix-substituted labels (Workflows.do_claim_next_job/2, Matrix.substitute/2).
  • Static matrices with include/exclude, fail-fast for queued legs (Matrix.expand/1, Workflows.maybe_fail_fast/1; running legs: #4’s plan).
  • push with branches/tags/paths, pull_request opened/synchronize/reopened, workflow_dispatch via the API, annotated tags (Workflows.dispatch/2, dispatch_pull_request/3).
  • Repository and organization secrets and variables (GitGud.Secrets.resolve_for/1), reserved GITHUB_*/GITEA_*/ FORGEJO_* names (Secrets.ReservedNames).
  • Logs per job and step (update_log/2 keyed by task id since 56749c0), live log streaming, re-run of a whole run or a single job (Workflows.rerun/1, rerun_job/2; the attempt counter is in the context plan), stale-job recovery (Workers.StaleJobReaper).
  • Cache lookup order and scoping on read (ActionsCache.lookup/5 filters by repository; PR fallback to the base branch is in the artifacts/cache plan).
  • Commit statuses on the commit, log and PR pages (core_components.ex commit_status_badges/1).

Suggested order

Artifacts-and-cache and job-token first (security). Then runner-context, job-graph, cancellation-and-concurrency (the silently-wrong core). Then checks, filter-patterns, matrix, triggers. Fork pull requests last (it needs job-token).