DynaCI and runtime-grounded CI: better GitHub Actions workflows
Most broken CI pipelines fail for a dull reason: the workflow file describes what someone thought the build needed, not what the build actually runs. DynaCI, a research prototype published in October 2026, generates GitHub Actions workflows from traces of real local runs. This post explains what the paper shows, what it does not, and how to apply the same idea to your own repositories this week, with or without the tool.
What DynaCI is, and what it is not
DynaCI is described in an arXiv preprint, DynaCI: Generating GitHub Actions Workflows via Dynamic Analysis, by Javid Ditty, Rahul Ghanshyam Chavan, Nimmi Weeraddana and Foyzul Hassan, submitted on 21 October 2026 [1]. It is a research system, not a product you can install today. The paper does not link a public code repository, and we could not find a released tool under that name. A preprint has not yet been through peer review, so treat the results as promising early evidence rather than settled fact.
The problem it targets is real and familiar. A CI (continuous integration) workflow is the YAML file in .github/workflows/ that tells GitHub Actions how to install, build and test your code on every push. Earlier generation approaches work from static material: natural-language descriptions, partial workflows, or patterns mined from other repositories. GH-WCOM, for example, is a Transformer model that suggests completions for partially written GitHub workflows (Mastropaolo et al., ICSE 2024) [2]. Static inputs miss the things nobody wrote down: the system package a test quietly needs, the environment variable a fixture reads, the exact command a maintainer types.
How it works
According to the paper, DynaCI works in three stages [1]:
- Trace the local run. A developer's normal build and test commands are run under
strace, a Linux tool that records system calls. Four probes extract the command sequence, how long each command took, which files were opened, and whichaptandpippackages those file paths belong to. A second run usesltraceto catch C librarygetenvcalls, plus a Pythonsitecustomizehook to catch reads ofos.environ. Two runs are needed becausestraceandltracecannot attach to the same process at once. - Build a draft with heuristics. Rules informed by that runtime data turn the trace into an initial workflow: which packages to install, which commands to run, which environment variables to set.
- Refine with an LLM. The draft is improved iteratively with retrieval-augmented generation (RAG), where a large language model is given retrieved reference material alongside its prompt. The authors used GPT-4o-mini for generation and ADA-002 for embeddings, and note that neither model is essential to the approach.
What the results tell you, and what they do not
The authors evaluated DynaCI on 50 popular Python projects that already use GitHub Actions, and compared it with GH-WCOM and LLM4GW, a set of prompting techniques for using LLMs on GitHub workflow tasks [1]. The headline numbers are below, with the context you need to read them.
| Reported result [1] | What it measures | Caveat |
|---|---|---|
| More than 21.13% higher BLEU on average | Token-level text similarity between the generated workflow and the project's existing human-written one | The authors state BLEU and F1 do not fully capture whether a workflow is correct |
| 20.96% higher F1 on average | Whether workflow components appear in both files with matching values | Matching the existing file is not the same as matching what the project needs |
| 56% of workflows had no missing functionality | A manual execution analysis of the generated workflows | Roughly 44% still needed repairs, though fewer than with the baselines |
| Python projects, Linux, Ubuntu runner | Scope of the evaluation | No evidence yet for other languages, macOS or Windows runners, or monorepos |
The practical reading: grounding generation in runtime evidence measurably helps, and even the best result in the paper leaves a large share of workflows needing a human to finish them. Generation gives you a better first draft. It does not remove the review step.
The practice you can use today: runtime-grounded CI
You do not need DynaCI to get most of its benefit. The core idea is simple: CI should run what developers actually run, discovered from evidence rather than memory. Here is a playbook that works for any language.
- Find the real commands. Start from a clean checkout in a fresh container, so nothing on your laptop leaks in, and run the build and tests until they pass. Note every package you had to install and every variable you had to set. That list is your dependency inventory.
- Optionally trace it. On Linux you can capture what the build executed with
strace -f -e trace=execve -o trace.log make test. The-fflag follows child processes, so you see every tool your test runner launches. Read the log, do not commit it: command arguments can include tokens. - Put the commands behind one entry point. Create a
make citarget or ascripts/ci.shscript. Developers and CI then run the same thing, and the workflow file stays thin. - Write or generate the draft. Hand-write it, start from a template, or ask an LLM. The source matters less than what happens next.
- Lint it. Run
actionlint, which checks workflow syntax,${{ }}expression types, action inputs and outputs, runner labels and cron syntax, runs shellcheck overrun:scripts, and flags script injection and hard-coded credentials [5]. - Run it locally.
actruns workflows on your machine in Docker containers.act -l pull_requestlists the jobs for an event, andact -j testruns one job [6]. Its default images do not contain all the tools GitHub's hosted runners have, and it uses containers rather than virtual machines [10], so treat a local pass as a strong hint, not proof. - Harden it. Apply the controls in the next section before the first merge, not after an incident.
- Review it as code. Add a
CODEOWNERSentry for.github/workflows/so a named owner approves every pipeline change.
A thin, hardened workflow
This is a corrected and hardened version of the kind of output the original post illustrated. It uses the current major versions of the official actions (v7 at the time of writing) [7][8], reads the Python version from a file, and delegates the real work to make ci. The permissions and concurrency keys are documented in GitHub's workflow syntax reference [4].
name: CI
on:
push:
branches: [main]
pull_request:
# Read-only token by default; raise per job only if needed.
permissions:
contents: read
# A newer commit on the same ref cancels the older run.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
# In production, pin each action to a full commit SHA
# and keep the tag in a comment, e.g. @<sha> # v7
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-python@v7
with:
python-version-file: '.python-version'
cache: 'pip'
- run: pip install -r requirements.txt
- run: make ci
Two settings deserve a note. actions/checkout stores the job token in the local Git config by default (persist-credentials defaults to true) [8]; turning it off means later steps cannot reuse it unless you mean them to. And setup-python recommends always setting the Python version explicitly, through python-version or python-version-file [7].
Hardening generated workflows
Generated workflows are written by something that does not know your threat model. GitHub's own secure-use reference sets out the controls to check [3]:
- Least-privilege token. Make sure
GITHUB_TOKENhas the minimum permissions it needs, defaulting to read-only contents and raising permissions per job. - Pin actions to a commit SHA. A tag can be moved; a full-length SHA cannot. Check that the SHA comes from the action's own repository, not a fork.
- No untrusted input in scripts. Values such as a pull request title can carry shell commands. Pass them through an intermediate environment variable instead of interpolating them into
run:. - Avoid
pull_request_targetunless you need it. Combined with checking out untrusted pull request code, it exposes the repository. GitHub suggestsworkflow_runfor privilege separation. - Prefer OIDC to stored cloud keys. OpenID Connect lets a workflow obtain short-lived, scoped cloud credentials, so you stop storing long-lived secrets.
The script injection fix looks like this:
# Unsafe: the title is pasted into the shell script.
- run: echo "Checking ${{ github.event.pull_request.title }}"
# Safe: the title arrives as data in an environment variable.
- env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: echo "Checking $PR_TITLE"
Once actions are pinned, keep them current. Dependabot can raise pull requests for outdated actions with a package-ecosystem: "github-actions" entry in .github/dependabot.yml [9].
Checklist for a new or generated workflow
- The workflow calls one entry point (
make cior a script) that developers also run. - Language and tool versions come from files in the repository, not from memory.
- Top-level
permissionsis set, withcontents: readas the default. - Every third-party action is pinned to a full commit SHA, with the tag in a comment.
- No
${{ github.event.* }}values are interpolated directly intorun:scripts. actionlintpasses locally and in CI.- Jobs have a
timeout-minutesvalue and aconcurrencygroup. - Cloud access uses OIDC rather than stored long-lived keys.
- A named owner approves changes under
.github/workflows/.
Common mistakes
- Trusting a similarity score. A workflow that looks like the old one can still miss a step. Run it.
- Generating from the README. READMEs drift. Traces and clean-container runs do not.
- Duplicating logic in YAML. When the workflow and the Makefile both define the test command, they will diverge. Keep YAML as glue.
- Merging generated workflows without review. On the paper's own numbers, a large share still need fixing [1].
- Tracing on a machine with real data. Run traces in a clean container. Logs can capture file paths and arguments you would not want stored, which matters for teams handling financial or health data.
What to do this week
- Run
actionlintacross one active repository and fix what it reports. - Add top-level
permissions: contents: readto every workflow that lacks apermissionsblock, then raise permissions only where a job fails. - Pick your most flaky pipeline and rebuild its dependency list from a clean-container run.
- Add a
CODEOWNERSrule for.github/workflows/. - Enable Dependabot for the
github-actionsecosystem [9].
Pipelines are production infrastructure: they hold credentials and decide what ships. If you want a second pair of eyes on how your CI handles tokens, secrets and supply-chain risk, our security team reviews exactly this kind of setup.
References
- Ditty, Chavan, Weeraddana and Hassan, "DynaCI: Generating GitHub Actions Workflows via Dynamic Analysis", arXiv:2610.29890 (21 October 2026)
- Mastropaolo, Zampetti, Bavota and Di Penta, "Toward Automatically Completing GitHub Workflows", ICSE 2024, arXiv:2308.16774
- GitHub Docs: Secure use reference (GitHub Actions)
- GitHub Docs: Workflow syntax for GitHub Actions
- rhysd/actionlint: static checker for GitHub Actions workflow files
- act user guide: usage
- actions/setup-python
- actions/checkout
- GitHub Docs: Keeping your actions up to date with Dependabot
- act user guide: runners