11.4 KiB · text History 95a46eb

CI gaps against GitHub / Forgejo Actions

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).