Skip to content

Latest commit

Β 

History

10 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

gh-deploy-control-with-issues

Open source deployment platform for GitHub Actions. Trigger production deploys from GitHub Issues using Issue Types and service labels, with optional approval via reactions, configurable health checks, and manual/automatic rollback.

Features

  • Centralized configuration in deploy.config.yaml β€” no hardcoded service names in workflows
  • Label-based deploy targets β€” each service key becomes a GitHub label
  • Issue form β†’ label sync β€” service checkboxes and optional deploy options are applied as labels automatically
  • Pluggable deploy strategies per service: ssh-docker, cloudflare-pages, script
  • Approval gate via πŸš€ reaction from authorized users
  • Rejection via πŸ‘Ž reaction
  • Manual rollback via πŸ‘€ reaction
  • Automatic rollback on deploy or health check failure (configurable)
  • Rollback notifications on the issue with @mentions, failure details, and log excerpts
  • Audit trail posted as issue comments
  • GitHub CLI first β€” gh, gh label, gh issue, gh api

Tutorial: adopt in an existing project

This guide shows how to integrate the platform into a repository that already has code and pipelines. Deploys are triggered by issues β€” this does not replace your existing build/CI, only the production release step.

Prerequisites

  • GitHub repository with Actions enabled
  • Permission to create repository secrets (and organization secrets, if applicable)
  • GitHub CLI β‰₯ 2.94 on runners (ubuntu-latest includes it)
  • For Issue Types in organization repositories: PAT with admin:org scope (see below)

Step 1 β€” Copy files from this repository

In your project, copy the structure below from gh-deploy-control-with-issues:

your-project/
β”œβ”€β”€ deploy.config.yaml              ← create from example (step 2)
β”œβ”€β”€ .github/
β”‚   β”œβ”€β”€ workflows/
β”‚   β”‚   β”œβ”€β”€ deploy.yml
β”‚   β”‚   β”œβ”€β”€ sync-resources.yml
β”‚   β”‚   └── ci.yml                  ← optional, recommended
β”‚   β”œβ”€β”€ scripts/                    ← entire folder
β”‚   β”œβ”€β”€ ISSUE_TEMPLATE/             ← optional (sync generates deploy.yml)
β”‚   └── deploy-scripts/             ← only if using strategy: script
└── actions/
    β”œβ”€β”€ deploy/action.yml           ← required router
    β”œβ”€β”€ deploy-ssh-docker/
    β”œβ”€β”€ deploy-cloudflare-pages/
    └── deploy-script/

Via terminal (with authenticated gh):

# At your repository root
OWNER=bunx-ai
REPO=gh-deploy-control-with-issues
TMP=$(mktemp -d)
gh repo clone "$OWNER/$REPO" "$TMP"

cp -R "$TMP/.github/workflows/deploy.yml" "$TMP/.github/workflows/sync-resources.yml" .github/workflows/
cp -R "$TMP/.github/scripts" .github/
cp -R "$TMP/actions" .
cp "$TMP/examples/deploy.config.example.yaml" deploy.config.yaml
mkdir -p .github/deploy-scripts
cp "$TMP/examples/deploy-scripts/worker.sh" .github/deploy-scripts/   # if using script strategy

# Optional: validation CI
cp "$TMP/.github/workflows/ci.yml" .github/workflows/

rm -rf "$TMP"

Tip: do this on a branch (feat/deploy-via-issues) and open a PR for review before merging to main.

Step 2 β€” Configure deploy.config.yaml

Edit the file at your project root. Each key under services becomes a GitHub label and a job in the deploy matrix.

  1. List the services you currently publish (API, frontend, worker, etc.)
  2. Choose a strategy for each (ssh-docker, cloudflare-pages, or script)
  3. Fill config with secret names the workflow will inject (do not put sensitive values in the YAML)
  4. Set deployment.approval.users to the GitHub usernames of approvers

Minimal example for a Docker backend over SSH:

deployment:
  issue_type: Deploy
  fallback_trigger_label: deploy
  approval:
    enabled: true
    users: [your-github-username]
  rollback:
    enabled: true
    automatic: true

services:
  api:
    image: ghcr.io/your-org/your-api
    strategy: ssh-docker
    config:
      ssh_host_secret: PRODUCTION_SSH_HOST
      ssh_user_secret: PRODUCTION_SSH_USERNAME
      ssh_key_secret: PRODUCTION_SSH_KEY
      container_name: api
    healthcheck:
      url: https://api.yourdomain.com/health

See examples/deploy.config.example.yaml in this repository for all options.

Step 3 β€” Map secrets in the workflow

deploy.config.yaml references secrets by name. The workflow must expose them as environment variables.

Open .github/workflows/deploy.yml and add your secrets to each env: block in the deploy, healthcheck, and rollback jobs:

env:
  PRODUCTION_SSH_HOST: ${{ secrets.PRODUCTION_SSH_HOST }}
  PRODUCTION_SSH_USERNAME: ${{ secrets.PRODUCTION_SSH_USERNAME }}
  PRODUCTION_SSH_KEY: ${{ secrets.PRODUCTION_SSH_KEY }}
  CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
  CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

Only include secrets your services actually use.

Step 4 β€” Create secrets in GitHub

Under Settings β†’ Secrets and variables β†’ Actions, create the secrets referenced in step 3 (SSH hosts, Cloudflare tokens, etc.).

For organization repositories using Issue Type Deploy:

Secret Scope Purpose
ORG_ADMIN_TOKEN PAT with admin:org Create Issue Type in the org (sync workflow)
Other secrets Deploy/infra SSH, Cloudflare, etc.

User-owned repositories (personal accounts) do not need ORG_ADMIN_TOKEN β€” sync creates the deploy label as a fallback.

Step 5 β€” Disable the old deploy workflow (if any)

If you had a monolithic workflow (e.g. cd.yml that deployed on every push or via issues with [DEPLOYMENT]):

  1. Disable or remove the old workflow to avoid concurrent deploys
  2. Move custom logic to:
    • strategy: script + a script in .github/deploy-scripts/, or
    • a new action in actions/deploy-<name>/ (see Adding a custom strategy)
  3. Compare with the table in Migration from legacy cd.yml

Your build/test CI can stay as-is β€” this platform only runs when a deploy issue is opened or labeled.

Step 6 β€” Sync labels, Issue Type, and template

Merge your branch to main and run Sync Deploy Resources (Actions β†’ Sync Deploy Resources β†’ Run workflow).

Sync will:

  • Create labels for each service (api, frontend, …)
  • Create Issue Type Deploy in the org (with ORG_ADMIN_TOKEN) or the deploy label (fallback)
  • Generate/update .github/ISSUE_TEMPLATE/deploy.yml

Step 7 β€” First test deploy

  1. Issues β†’ New issue β†’ Deploy
  2. Select the test service (e.g. api) and describe the reason
  3. If approval.enabled: true, a user in approval.users reacts with πŸš€
  4. Monitor in Actions β†’ Deploy

Manual test (without opening a new issue):

gh workflow run deploy.yml -f issue_number=123

Replace 123 with a valid issue number (type Deploy + service label).

Quick checklist

Item Done?
.github/ and actions/ files copied ☐
deploy.config.yaml with your services ☐
Secrets mapped in deploy.yml ☐
Secrets created in the repository ☐
ORG_ADMIN_TOKEN (org + Issue Type only) ☐
Old deploy workflow disabled ☐
Sync Deploy Resources run ☐
Test issue with successful deploy ☐

Coexisting with the rest of your project

  • Monorepo: one service per key in services; labels select what to deploy for that issue
  • Images: set image to the desired tag; the workflow uses the current commit SHA/ref for the changelog β€” adjust scripts if you always use latest
  • Other workflows: no conflict; deploy uses concurrency per issue number
  • Existing issue templates: the Deploy template coexists with yours; config.yml only disables blank issues if you copy ours

Quick start

1. Configure services

Copy the example and edit deploy.config.yaml at the repository root:

cp examples/deploy.config.example.yaml deploy.config.yaml
deployment:
  issue_type: Deploy
  approval:
    enabled: true
    users: [techlead, sre]
  rollback:
    enabled: true
    automatic: true
  healthcheck:
    enabled: true
    timeout: 300
    retries: 10
  observability:
    include_failed_logs: true
    max_log_lines: 40

services:
  backend:
    image: ghcr.io/company/backend
    strategy: ssh-docker
    config:
      ssh_host_secret: PRODUCTION_SSH_HOST
      ssh_user_secret: PRODUCTION_SSH_USERNAME
      ssh_key_secret: PRODUCTION_SSH_KEY
      container_name: backend
    healthcheck:
      url: https://api.example.com/health

2. Sync GitHub resources

Run the Sync Deploy Resources workflow (or push deploy.config.yaml to main):

  • Creates labels for each service (frontend, backend, worker, …)
  • Organization repos: creates Issue Type Deploy via REST API (requires secret ORG_ADMIN_TOKEN)
  • User-owned repos: Issue Types cannot be created via API β€” the workflow creates a fallback label deploy instead

Organization setup (Issue Types)

GITHUB_TOKEN cannot create Issue Types β€” it lacks admin:org scope. For organization repositories:

  1. Create a Personal Access Token with admin:org scope (org owner/admin required)
  2. Add it as repository secret: ORG_ADMIN_TOKEN
  3. Run Sync Deploy Resources again

Without ORG_ADMIN_TOKEN, the sync workflow fails with a clear error and creates the fallback label deploy so deploys can still work via label.

Requires GitHub CLI β‰₯ 2.94 for issue type support.

3. Map secrets in the deploy workflow

Edit .github/workflows/deploy.yml and add secrets referenced in your config to the deploy and rollback job env blocks:

env:
  PRODUCTION_SSH_HOST: ${{ secrets.PRODUCTION_SSH_HOST }}
  PRODUCTION_SSH_USERNAME: ${{ secrets.PRODUCTION_SSH_USERNAME }}
  PRODUCTION_SSH_KEY: ${{ secrets.PRODUCTION_SSH_KEY }}

4. Open a deploy issue

Use the Deploy issue template (generated from deploy.config.yaml by the sync workflow):

  1. New issue β†’ Deploy
  2. Check the services to deploy (labels are synced automatically when the workflow starts)
  3. The template sets Issue Type Deploy (org) and label deploy (fallback)
  4. If approval is enabled, an authorized user reacts with πŸš€

The template is kept in sync when you change services in deploy.config.yaml β€” run Sync Deploy Resources or push to main.

CLI alternative:

gh issue create \
  --type Deploy \
  --title "[Deploy] Release v1.2.0" \
  --label deploy \
  --label backend \
  --body "Reason: merged PR #42"

Reaction reference

Emoji Meaning
πŸš€ Approve deploy
πŸ‘Ž Reject deploy (closes issue)
πŸ‘€ Request manual rollback

Deploy strategies

Strategy Description Config keys
ssh-docker SSH to host, docker pull, restart container ssh_host_secret, ssh_user_secret, ssh_key_secret, ssh_port_var, container_name
cloudflare-pages Deploy static directory via Wrangler project_name, directory (+ CLOUDFLARE_* secrets in workflow)
script Run a repository script script (path relative to repo root)

Script strategy scripts may record refs for rollback/changelog by writing to temp files before exit:

echo "$PREVIOUS_REF" > "/tmp/deploy-previous-ref-${SERVICE}"
echo "$DEPLOYED_REF" > "/tmp/deploy-deployed-ref-${SERVICE}"

If these files are absent, deployed_ref falls back to the IMAGE input.

Multi-repo script deploys

The workflow checkout is the control repository (workflows, config, scripts). The deployed application may live in a different repo. Set config.git_remote (and optionally config.git_branch) in deploy.config.yaml; the script receives the full config as CONFIG_JSON during deploy and rollback.

See examples/deploy-scripts/multi-repo-git-sync.sh for a generic git-sync pattern with marker-file validation, safe rollback (refs must belong to origin/<branch>), and ref recording.

Rollback refs must belong to the deployed repository. If origin was previously pointed at a different repo, a stale SHA can break the tree β€” the example script falls back to origin/<branch> when the ref is not an ancestor.

Health check URLs and redirects

By default, health checks accept HTTP 2xx only. SPA root URLs often return 307/302 redirects, which fail unless configured otherwise.

Recommended: point healthcheck.url at a path that returns 2xx directly (/health, /sign-in, API health endpoint).

Optional per-service overrides in deploy.config.yaml:

services:
  frontend:
    healthcheck:
      url: https://app.example.com/
      follow_redirects: true   # curl -L; final response must still match accept rules
      # OR accept redirect status without following:
      # accept_status: [200, 301, 302, 307, 308]

When accept_status is set, the HTTP code is matched against that list instead of the default 2xx range. When neither option is set, behavior is unchanged (2xx only).

Adding a custom strategy

  1. Create actions/deploy-<name>/action.yml with outputs: previous_ref, deployed_ref, deploy_status, failure_detail
  2. Register the strategy in actions/deploy/action.yml
  3. Set strategy: <name> in deploy.config.yaml

Workflows

Workflow Trigger Purpose
.github/workflows/ci.yml Push/PR to main, manual Validate scripts and deploy.config.yaml
.github/workflows/validate-deploy.yml Push/PR (deploy paths), manual Validate deploy platform files and generated issue template
.github/workflows/sync-resources.yml Manual, push to deploy.config.yaml or sync scripts Provision labels, issue types, and deploy issue template
.github/workflows/deploy.yml Issue opened, manual (with issue_number) Full deploy pipeline

Important: deploy does not run on every commit. It triggers when:

  1. A deploy issue is opened (one run per issue β€” not on labeled events), or
  2. You manually run Deploy in Actions with an issue_number input (for retries).

Every push to main runs CI to validate the repository. Changes under deploy paths also trigger Validate Deploy (path-filtered).

Retrying a deploy

labeled events no longer trigger the workflow (this prevents duplicate runs when the bot syncs service labels). To retry after a failure or partial deploy:

  1. Open Actions β†’ Deploy β†’ Run workflow
  2. Enter the issue number
  3. If approval is enabled, react with πŸš€ on the new "Waiting for approval" comment (only reactions after that comment count; old πŸš€ reactions are ignored)

Concurrency is set to cancel-in-progress: true β€” a new run cancels any in-progress run for the same issue.

Issue comments and GH_TOKEN

Any workflow step that posts issue comments (via audit.sh, gh issue comment, etc.) must have GH_TOKEN, GITHUB_REPOSITORY, and ISSUE_NUMBER in its environment. The deploy workflow sets these at job level on wait-approval, healthcheck, rollback, manual-rollback, and finalize. When adding custom steps that audit to the issue, include the same env block.

Issue form and label sync

GitHub issue forms do not map checkboxes to labels automatically. When a deploy issue is opened:

  1. Sync labels from issue form reads checked service boxes (and optional deploy-option boxes) from the issue body
  2. Missing labels are created and applied before validation

Label sync does not re-trigger the workflow β€” only opened and workflow_dispatch start a deploy run.

You can still add service labels manually or via CLI β€” both labels and checked boxes are recognized.

Optional deployment.issue_template

Customize strings in deploy.config.yaml; Sync Deploy Resources regenerates .github/ISSUE_TEMPLATE/deploy.yml:

deployment:
  issue_template:
    intro: |
      Multi-line markdown shown at the top of the deploy form.
    services_description: Select all services that should be deployed.
    reason_placeholder: Deploy v1.2.0 after merging PR #42
    notes_description: Expected rollback, dependencies, maintenance window...

Optional per-service deploy options

Generalize optional steps (e.g. database migrations) with options.deploy_options:

services:
  backend:
    options:
      deploy_options:
        - id: migrations
          label: migrations          # GitHub label applied when checked
          checkbox: Run database migrations
          description: Only when this service is selected.

Deploy scripts can read the corresponding label on the issue to decide whether to run the option.

Composite action output handling

The deploy router (actions/deploy/action.yml) maps strategy outputs directly instead of re-emitting them through a collect step. Plain echo key=value breaks GITHUB_OUTPUT when failure_detail contains |, #, or multi-line Docker logs, and empty multiline heredocs can fail the step even on success. Strategy steps use continue-on-error: true; a final Verify deploy status step fails the action only when deploy status is failure or missing.

Architecture

Issue (Type: Deploy + labels)
  β†’ setup (sync labels + validate + matrix)
  β†’ wait-approval (optional, poll πŸš€/πŸ‘Ž after approval comment timestamp)
  β†’ deploy (matrix per service β†’ strategy action β†’ per-service outcome artifact)
  β†’ deploy-summary (aggregate succeeded/failed services)
  β†’ healthcheck (matrix, gated per service deploy outcome)
  β†’ rollback-notify (comment on issue with failure details + @mentions)
  β†’ rollback (per service, only when that service failed)
  β†’ finalize (per-service outcomes β†’ success / partial / rollback summary)

Partial deploy handling

When deploying multiple services (fail-fast: false), each service is tracked independently:

  • Backend OK + frontend fail β†’ backend stays deployed; only frontend is rolled back
  • Issue stays open with a partial deploy comment listing succeeded vs failed services
  • Automatic rollback runs only for services that failed deploy or health check

Full success closes the issue. Full failure with successful rollback closes the issue and adds deploy:rolled-back.

Rollback notifications

When a deploy or health check fails and automatic rollback is enabled:

  1. Immediate alert on the issue with @mentions (issue author + approval.users)
  2. Failure details per service (HTTP status, deploy error, etc.) in collapsible <details> blocks
  3. Log excerpt from failed workflow steps via gh run view --log-failed
  4. Per-service rollback updates as each service is restored
  5. Final summary distinguishing:
    • Environment restored (deploy:rolled-back label, issue closed automatically)
    • Rollback also failed (manual intervention required)
    • Partial rollback (some services could not be restored)

Configure log inclusion in deploy.config.yaml:

deployment:
  observability:
    notify_on_success: true
    include_failed_logs: true
    max_log_lines: 40
    max_log_chars: 3500
    changelog:
      enabled: true
      max_commits: 20
      state_variable: DEPLOY_LAST_GIT_SHA

Success notifications

When deploy and health checks succeed:

  1. Optional success comment on the issue (notify_on_success, default on) with @mentions
  2. Changelog link comparing the current commit with the last successful deploy (changelog.enabled, default on)
  3. Collapsible commit summary in the issue comment
  4. Issue is closed automatically

The last deployed Git SHA is stored in the repository variable DEPLOY_LAST_GIT_SHA (configurable via changelog.state_variable). The workflow needs actions: write permission on the finalize job to persist it. On the first deploy, the changelog falls back to parsing image tags from the previous deployment state.

Disable success notifications:

deployment:
  observability:
    notify_on_success: false

Migration from legacy cd.yml

Legacy New platform
[DEPLOYMENT] title prefix Issue Type Deploy
Checkbox targets in issue body Service labels from config (auto-synced on issue open)
Re-trigger on label change workflow_dispatch with issue_number (retry flow)
vars.ALLOWED_USERS_* deployment.approval.users
Per-service hardcoded jobs Dynamic matrix + strategies
curl GitHub API calls gh issue comment, gh issue close
Inline SSH health checks healthcheck.sh + config

The legacy monolithic workflow (cd.yml) has been removed. See examples/ for reference configuration.

Upgrading from earlier template versions

Breaking changes when syncing this template into an existing project:

Change Action required
labeled no longer triggers deploy Retries use Actions β†’ Deploy β†’ Run workflow with the issue number. Re-approve with πŸš€ on the new approval comment.
Partial multi-service deploys The issue may stay open when only some services fail; successful services are left running. Only failed services are rolled back.
Approval reactions are time-scoped Only πŸš€ reactions after the latest "Waiting for approval" comment count. Old reactions are ignored and removed after approval.
deploy-summary job Ensure actions/checkout runs before sourcing .github/scripts/lib/*.sh in summary/finalize jobs.
Healthcheck for SPAs Use a 2xx URL or set follow_redirects / accept_status per service.
Script strategy rollback Scripts receive CONFIG_JSON; multi-repo deploys should validate rollback refs belong to origin/<branch>.

Requirements

  • GitHub Actions
  • GitHub CLI (gh) on runners (pre-installed on ubuntu-latest)
  • yq (installed by workflows)
  • Repository permissions: issues: write, contents: read, deployments: write

Credits

This platform builds on the original concept and base workflow created by @scarletquasar. The open-source implementation in this repository extends and generalizes that foundation.

Feito no Brasil

License

Open source β€” customize deploy.config.yaml and extend strategies for your infrastructure.

About

Open source deployment platform for GitHub Actions. Trigger production deploys from GitHub Issues using **Issue Types** and **service labels**, with optional approval via reactions, configurable health checks, and manual/automatic rollback.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors