Add GitHub workflows to a repo
Give a repo the pipeline its siblings already have, translated to its own stack.
Two failure modes bracket this task. Copying a sibling verbatim gives a frontend a mvn verify
it cannot run. Writing a generic pipeline from scratch ignores the org's release conventions, so
the repo versions, tags, and deploys differently from everything around it. The job is to keep the
shape and replace the steps.
Phase 1 — Read the org's existing pipeline
Never start from a blank file. Find the closest sibling repo that already has workflows:
ls ../*/.github/workflows/ 2>/dev/null
cat ../<sibling>/.github/workflows/*.yml
Extract the conventions and write them down before generating anything:
- File split — typically one workflow per trigger (
pr.ymlfor validation,dev.yml/main.ymlfor release). Match the naming and the trigger branches. - Version scheme. This is the highest-risk thing to get wrong because it is invisible from the
code. In Dracolich the bump is parsed from the commit subject:
feature*→ major,update*→ minor,fix*→ patch, anything else → no release at all. Reproduce the logic exactly, including theshould_releaseshort-circuit, or this repo will version out of step with every sibling. - Job chain and its order — what depends on what, and which steps are irreversible.
- Artifact destinations — package registry, image registry and tag format, and any downstream repo that gets written to.
- Secrets used, by name.
- Permissions blocks per workflow.
Phase 2 — Detect the repo's real stack
Read the manifest; don't assume from the repo name.
ls package.json pom.xml build.gradle* pyproject.toml go.mod Cargo.toml *.csproj 2>/dev/null
Then determine, from files rather than convention:
- Runtime version —
engines.node/.nvmrc/<java.version>/python-versionin a config. Pin the same version the project actually targets. - Which scripts genuinely exist. Read
scriptsinpackage.json, or the build file's tasks. Never invent a step for a script that isn't there — anpm teststep in a repo with no test runner fails the pipeline on its first run and teaches everyone to ignore red builds. - Whether tests exist at all. Zero test files means no test step, and say so in your report rather than silently omitting it.
- Lockfile presence —
npm cirequirespackage-lock.json; without one it must benpm install. - What the deployable artifact is. This is the fork that decides the whole release half of the pipeline (see Phase 4).
Phase 3 — Map the stages
Keep the sibling's job chain; swap each stage's implementation.
| Stage | JVM / Maven | Node / TS | Python | Go | .NET |
|---|---|---|---|---|---|
| Toolchain | actions/setup-java + distribution |
actions/setup-node + cache: npm |
actions/setup-python |
actions/setup-go |
actions/setup-dotnet |
| Install | (implicit) | npm ci |
pip install -e .[dev] / poetry install |
go mod download |
dotnet restore |
| Registry auth | settings.xml for GitHub Packages |
.npmrc — only if it consumes/publishes private packages |
index URL | GOPRIVATE |
nuget source |
| Lint | (often none) | npm run lint |
ruff / flake8 |
go vet, golangci-lint |
dotnet format --verify-no-changes |
| Type check | (compiler) | npx tsc --noEmit |
mypy |
(compiler) | (compiler) |
| Test | mvn -B test |
npm test (only if it exists) |
pytest |
go test ./... |
dotnet test |
| Build | mvn -B package |
npm run build / framework export |
python -m build |
go build |
dotnet publish |
| Publish | mvn deploy |
npm publish |
twine |
release binaries | dotnet nuget push |
Drop stages that don't apply rather than finding something to put there. A JVM repo's
GitHub-Packages settings.xml block has no Node equivalent unless the project actually consumes
private npm packages — check before adding an .npmrc step.
Phase 4 — Decide what the release half produces
For a backend service the answer is usually "a container image, then bump the chart". For a frontend or app it genuinely forks, and you must establish which before writing the release workflow — ask if the repo doesn't settle it:
- Static web bundle → container — build (
npm run build,expo export --platform web,vite build), copy into an nginx/static image, push, bump the chart. Mirrors the backend pipeline. - Static web bundle → static host — Pages, S3/CloudFront, Netlify. No image, no chart bump.
- Mobile app → store — a different pipeline entirely (EAS Build/Submit, Fastlane, signing credentials, store API keys). Do not fold this into the same job as a web build; the triggers, secrets, and cadence all differ.
- Library — publish to a registry; no image at all.
An Expo repo can be two of these at once (web bundle to the cluster, native builds to stores). If so, that is two workflows, not one with branches.
Also note: env vars baked at build time (EXPO_PUBLIC_*, VITE_*, NEXT_PUBLIC_*) must be
present in the build job, not the runtime container. Missing them produces a silently misconfigured
bundle rather than a failure.
Phase 5 — Write the workflows
Start from the sibling file, edit it down, and keep unchanged everything that isn't stack-specific:
trigger branches, permissions, concurrency, the version-bump script, job dependencies, and the
downstream bump job.
Validate before reporting done:
python3 -c "import yaml,sys;[yaml.safe_load(open(f)) for f in sys.argv[1:]];print('YAML OK')" \
.github/workflows/*.yml
command -v actionlint >/dev/null && actionlint .github/workflows/*.yml
Then read each run: line and ask whether that command would succeed in a clean checkout of this
repo. Most breakage is a command that doesn't exist here, not malformed YAML.
Phase 6 — Secrets and first-run safety
List every secret the workflows reference and check it exists at repo or org level. The user must add any that don't — you cannot, and you should never put a secret value in a workflow file.
Order matters more than it looks. In a chain like
tag → publish-package → publish-image → bump-chart, the tag and GitHub Release are pushed before
the later jobs run. A missing registry secret therefore leaves a tag and a release with no artifact
behind them — a half-release someone has to clean up by hand. Before the first run on the default
branch, either confirm every secret is present, or trigger it once via workflow_dispatch on a
branch where the irreversible steps are skipped.
Say explicitly in your report that the pipeline is unverified until it runs, and offer to watch the first execution.
Calibration
- Match the org's conventions even when you'd design it differently. Consistency across sibling repos is worth more than your preferred layout. Note improvements; don't apply them uninvited.
- No step without a command that exists. Check
scripts, check the test directory, check the lockfile. - Don't add a deploy step you can't ground. If you don't know where the artifact goes, ask. Guessing produces a pipeline that pushes something somewhere nobody intended.
- Don't run the pipeline to "test" it. Pushing to a release branch triggers a real release. Use
YAML validation,
actionlint, and reading — and let the user decide when to run it. - Pin action versions to the major the siblings use, so all repos update together.