| name | smaqit.infrastructure-cicd-generate |
| description | Use when manually generating or updating the canonical 3-workflow GitHub Actions CI/CD set (deploy.yml, provision.yml, post-merge-deploy.yml) for a Node.js + React application deployed to a VM via Terraform and Docker Compose. Also use when an operator invokes /cicd.generate, or when updating existing workflows to match project conventions. In `smaqit.new-greenfield-project`, the deployment agent generates workflows during Phase 5 using these patterns as reference — invoke this skill directly only when generating CI/CD outside the zero-to-prod flow. Note: `copilot-setup-steps.yml` is provided by the smaqit framework and is not generated by this skill. |
| metadata | {"version":"2.0.0"} |
CI/CD Workflow Generator
Generates workflows by copying real, pre-validated template files from assets/ and
substituting one token — it does not synthesize YAML from a prose description. This is
deliberate: every Gotcha below used to be something an agent had to remember to reproduce
correctly on every generation; now it's encoded once, in a template that's already been
validated with actionlint/yamllint/shellcheck (Task 085).
Generation Mode
full (default) — this project provisions and manages its own VM's Terraform state.
Generates all 3 workflows with a provision job.
deploy-only — provisioning_mode: existing-shared (see smaqit.new-greenfield-project
Phase 4/5): this project deploys onto a VM a different project owns and provisions. No
Terraform step is generated at all, and provision.yml is not generated.
Steps
-
Read existing workflows from .github/workflows/. If workflows exist and are substantially complete, offer to diff rather than overwrite.
-
Resolve configuration values from Infrastructure specs and AGENTS.md (or legacy platform-specific instructions). Only one substitution is needed — the templates otherwise use fixed, documented secret/variable names (see the table below and smaqit.infrastructure-repo-config):
| Token | Resolves to | Default if not found |
|---|
__APP_DIR__ | The remote application directory on the VM (e.g. /opt/myapp) | /opt/<project-slug> — derive <project-slug> from AGENTS.md or legacy platform instructions, same convention as smaqit.infrastructure-vault-loader |
All other references (VM_HOST, VM_SSH_KEY, TF_BACKEND_ACCESS_KEY, OS_APPLICATION_CREDENTIAL_ID, GH_TERRAFORM_TOKEN, VITE_DEMO_MODE, etc.) are baked into the templates verbatim — do not re-derive or rename them per project.
-
Select and copy the template variant for the active generation mode:
| File | full | deploy-only |
|---|
deploy.yml | assets/deploy.yml.full.template | assets/deploy.yml.deploy-only.template |
provision.yml | assets/provision.yml.template | not generated |
post-merge-deploy.yml | assets/post-merge-deploy.yml.template | assets/post-merge-deploy.yml.template (identical in both modes) |
-
Substitute __APP_DIR__ in the copied deploy.yml with the resolved value from Step 2 (provision.yml and post-merge-deploy.yml don't reference it).
-
Vendor the guard scripts — full mode only, since deploy-only has no Terraform/provision
job to guard. Copy, verbatim, into the target repo:
smaqit.infrastructure-provision-cyso/scripts/plan-guard.sh → deployment/scripts/plan-guard.sh
smaqit.infrastructure-provision-cyso/scripts/ownership-guard.sh → deployment/scripts/ownership-guard.sh
These are generated output, overwritten on every run — the skill directory's copies are
canonical (see smaqit.infrastructure-provision-cyso). The generated deploy.yml/provision.yml
reference deployment/scripts/plan-guard.sh and deployment/scripts/ownership-guard.sh at this
vendored, checked-in path — not a path that only exists in the smaqit skill directory, which a
GitHub-hosted runner has no access to.
-
Write files to .github/workflows/ — 3 workflow files + 2 vendored scripts (full) or 2
workflow files + 0 vendored scripts (deploy-only: deploy.yml + post-merge-deploy.yml, no
provision.yml, no Terraform to guard).
-
Report the list of files written with a brief description of each, including which generation mode was used.
Output
full: .github/workflows/deploy.yml (provision + deploy sequential jobs), provision.yml (PR-only terraform plan with PR comment), post-merge-deploy.yml (body sentinel trigger for deploy.yml); deployment/scripts/plan-guard.sh and deployment/scripts/ownership-guard.sh vendored
deploy-only: .github/workflows/deploy.yml (single deploy job, no Terraform), post-merge-deploy.yml (body sentinel trigger for deploy.yml) — no provision.yml, nothing vendored
Scope
- Does NOT set up GitHub Secrets or Variables — use
smaqit.infrastructure-repo-config for that.
- Does NOT provision infrastructure — Terraform runs inside the generated workflow (
full mode only; deploy-only mode has no Terraform at all).
- Does NOT handle matrix builds, monorepos, or container registry pushes — the templates assume exactly
backend/ + frontend/ as source directories (same assumption smaqit.infrastructure-deploy-rsync makes).
- Does NOT template anything beyond
__APP_DIR__. If a project needs more variance than that, treat it as a new template variant, not a general templating engine.
Examples
Input: New project with no CI/CD workflows, provisioning_mode: provision. Operator invokes /cicd.generate.
Output: 3 files written to .github/workflows/ (deploy.yml with sequential provision→deploy jobs, provision.yml as PR-only plan with PR comment, post-merge-deploy.yml with body sentinel), plus deployment/scripts/plan-guard.sh and deployment/scripts/ownership-guard.sh vendored.
Input: Co-hosted deploy, provisioning_mode: existing-shared. Operator invokes /cicd.generate.
Output: 2 files written to .github/workflows/ (deploy.yml with a single deploy job, post-merge-deploy.yml) — no provision.yml, nothing vendored, since there's no Terraform to guard.
Gotchas
These are now encoded directly in the assets/*.template files — kept here as maintainer-facing
documentation of why the templates look the way they do, for whoever edits the templates
themselves. They are no longer instructions an agent needs to reconstruct from memory on every
generation.
GITHUB_TOKEN is reserved — GitHub Actions runner injects its own short-lived installation token under GITHUB_TOKEN before any step executes, overwriting any env.GITHUB_TOKEN set by the workflow. The templates use TF_VAR_github_token; Terraform auto-maps TF_VAR_* → var.*.
- PR body sentinel, not label — Copilot Coding Agent tokens lack
issues:write. post-merge-deploy.yml.template uses contains(github.event.pull_request.body, 'smaqit:deploy'), not a label check. Do not change this to a label trigger.
provision.yml.template also carries TF_VAR_github_token — omitting it causes interactive prompt hangs in PR preview plans.
VITE_* vars are build-time — VITE_DEMO_MODE (and any other VITE_* var) is set as env: on the frontend build step specifically, never the deploy step. Vite bakes them at compile time; they are not available at runtime. Do not move this env block to a later step when editing the template.
.terraform.lock.hcl must be committed — do NOT add it to .gitignore. If CI fails with provider version errors, check that this file is tracked in git.
- Never remove the plan-guard/ownership-guard steps from
deploy.yml.full.template or provision.yml.template — a bare terraform apply -auto-approve with no gate in front of it is exactly the failure mode Task 084/085 exist to prevent. A user_data edit (even a comment-only one) forces VM replacement, and an ungated apply in CI carries it out unattended.
systemctl reload-or-restart nginx, never a bare reload — cloud-init's enable only arranges start-on-next-boot, so nginx may never have actually started. This is baked into both deploy.yml templates; don't simplify it back to reload.
{{ ... }} vs __TOKEN__ — the one substitution token this skill uses (__APP_DIR__) is deliberately not double-curly-brace shaped, to avoid colliding visually (and, for yamllint, semantically — see yamllint's braces rule) with GitHub Actions' own ${{ ... }} expression syntax, which appears throughout every template. Keep any future token in the same __TOKEN__ shape.
Completion
Failure Handling
| Situation | Action |
|---|
| Required input not provided | Request the missing information before proceeding |
| Gathered input is ambiguous | Flag the ambiguity and ask for clarification |
| Subagent invocation fails | Report the failure with context; do not silently retry |
| Output artifact already exists | Confirm with user before overwriting |
| Workflows already exist and differ | Show a diff summary; ask whether to overwrite or merge |
| Infrastructure spec not available | Default __APP_DIR__ to /opt/<project-slug>; note the value used in the report |
A template's content needs to differ from the shipped assets/*.template for this specific project | Treat that as a reason to add a new template variant to this skill (or flag it as a limitation), not to hand-edit the copied output — hand edits silently diverge from the validated template on the next re-generation |
deploy-only mode requested but a provision.yml already exists from a prior full generation | Delete the stale provision.yml and the vendored deployment/scripts/*.sh — leaving them in place would plan/guard Terraform that no longer matches this project's provisioning_mode |
deployment/scripts/plan-guard.sh or ownership-guard.sh already exist and differ from the skill directory's current copies | Overwrite — they are generated output, regenerated on every run (see Step 5) |