Skip to content

CI/CD Guide — middag-io

Complete reference for GitHub Actions, 1Password secrets, AWS deploy, and legacy Bitbucket migration.

Table of Contents


1. Overview

Architecture

Developer ──push──> GitHub (middag-io/{repo})


               GitHub Actions
               (reusable workflows from middag-io/.github-private)

            ┌────────────┼───────────┐
            ▼            ▼           ▼
        1Password      GHCR      Cloudflare
        (secrets)     (images)     (Pages)
            │            │
            ▼            ▼
         EC2 + 1Password Connect
         (production / staging)

Key Decisions

DecisionReference
Repo namingADR-001
Branch model (main + develop)ADR-002
Release strategy (release-please)ADR-003
Migration strategyADR-004
1Password integrationADR-005

2. 1Password — Day-to-Day Usage

Two Modes

ContextMechanismWhen to Use
CI (GitHub Actions)Service Account TokenWorkflows, build, test, deploy scripts
Servers (EC2/Docker)1Password ConnectProduction/staging containers, op inject

For Developers: How secrets flow

1. Secret lives in 1Password vault (e.g., CI-MYPROJECT)
2. Code references it as op://CI-MYPROJECT/item-name/field-name
3. In CI: 1password/load-secrets-action resolves it
4. On server: op inject resolves it via Connect
5. Developer never sees or copies the secret value

Adding a secret to a project

  1. Create the item in the appropriate 1Password vault:

    VaultUse for
    CI-SHAREDShared across all projects (GHCR, SES, Cloudflare global)
    CI-AWSAWS credentials (ECR, RDS, Lambda)
    CI-{PROJECT}Project-specific (deploy keys, API keys, certificates)
    PRIVATEPrivate keys, certificates
  2. Reference it in your .env.*.tpl or workflow:

    # In .env.production.tpl
    MY_SECRET="op://CI-MYPROJECT/my-item/my-field"
    yaml
    # In GitHub Actions workflow
    - uses: 1password/load-secrets-action@v2
      env:
        OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SA_MYPROJECT }}
        MY_SECRET: op://CI-MYPROJECT/my-item/my-field
  3. Verify the vault is accessible by the correct Service Account or Connect server.

Vault access changes

ContextWhat to do
Connect (server)1Password web → Integrations → Connect server → add vault. No redeploy.
Service Account (CI)If vault already in SA scope: nothing. If new vault needed: create new SA token, update GitHub secret.

Service Account naming

sa-github-ci-shared          → org-level (CI-SHARED + CI-AWS)
sa-github-ci-myproject   → my-project project
sa-github-ci-{project}       → new project

GitHub secret placement

SecretLevelWhere
OP_SERVICE_ACCOUNT_TOKENOrgGitHub org settings → Secrets → Actions
OP_SA_{PROJECT}RepoGitHub repo settings → Secrets → Actions
OP_CONNECT_TOKENServerEC2 environment (not in GitHub)

3. GitHub Actions — Reusable Workflows

All CI/CD workflows live in middag-io/.github-private and are called by individual repos.

Available Workflows

WorkflowPurposeUsed by
wp-plugin-ci.ymlPHP matrix + optional UI checks (configurable via inputs)wp-plugin-* repos
wp-theme-ci.ymlPHP matrix for themeswp-theme-* repos
wp-plugin-post-release.ymlBuild dist ZIP, upload to GH Release, trigger privatesatiswp-plugin-* repos
release-please.ymlVersion bump, changelog, GitHub ReleaseAll repos
docs-deploy.ymlVitePress build + Cloudflare Pages deploy (hash-gated)Repos with docs
docker-build-push.ymlMulti-arch build, GHCR push, optional ECR copydocker-* repos (planned)
docker-deploy.ymlSSH deploy to EC2docker-* repos (planned)
moodle-plugin-ci.ymlmoodle-plugin-ci checksmoodle-* repos (planned)
composer-package-ci.ymlPHPUnit, PHPStan, PHPCSPHP library repos (planned)

How to use in your repo

yaml
# .github/workflows/ci.yml
name: CI
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  ci:
    uses: middag-io/.github-private/.github/workflows/wp-plugin-ci.yml@workflows-v1
    with:
      has-ui: true
      has-tests: true
      has-rector: true
    secrets: inherit
yaml
# .github/workflows/release.yml
name: Release
on:
  push:
    branches: [main]

jobs:
  release-please:
    uses: middag-io/.github-private/.github/workflows/release-please.yml@workflows-v1
    permissions:
      contents: write
      pull-requests: write
    secrets: inherit

  post-release:
    needs: release-please
    if: needs.release-please.outputs.release_created == 'true'
    uses: middag-io/.github-private/.github/workflows/wp-plugin-post-release.yml@workflows-v1
    with:
      tag-name: ${{ needs.release-please.outputs.tag_name }}
      has-ui: true
      has-strauss: true
      zip-name: my-plugin
    permissions:
      contents: write
    secrets: inherit

Workflow inputs and variables

Per-repo configuration is now passed as workflow inputs (not repo variables). Org-level secrets and variables are shared via secrets: inherit.

Workflow inputs (per-repo, passed in with:)

InputWorkflowPurposeExample
zip-namewp-plugin-post-releasePlugin folder name for ZIP archivemy-project
has-uiwp-plugin-ci / post-releasePlugin has ui/ directorytrue
has-strausswp-plugin-post-releaseStrauss vendor prefixingtrue
has-testswp-plugin-ciRun composer testtrue
has-phpstanwp-plugin-ci / themeRun PHPStan static analysistrue
has-cs-fixerwp-plugin-ci / themeRun PHP CS Fixertrue
has-rectorwp-plugin-ci / themeRun Rectorfalse
needs-satis-authwp-plugin-post-releaseComposer needs privatesatis authtrue
docs-pathdocs-deployVitePress docs directorydocs/
project-namedocs-deployCloudflare Pages project namemy-project-docs

Org secrets (shared via secrets: inherit)

SecretVisibilityPurpose
OP_SERVICE_ACCOUNT_TOKENALL1Password Service Account (org-wide, CI-SHARED + CI-AWS)
PRIVATESATIS_PASSWORDALLHTTP Basic password for privatesatis.middag.com.br
PRIVATESATIS_DISPATCH_TOKENALLGitHub PAT with repo scope — triggers repository_dispatch on my-satis-repo
CLOUDFLARE_API_TOKENALLCloudflare API token (Pages deploy)
DOCKERHUB_TOKENALLDocker Hub push token
NPM_TOKENPRIVATEnpm registry publish token
SMTP_PASSWORDPRIVATESES SMTP password
PRIVATESATIS_BOT_APP_IDALLGitHub App ID for privatesatis-bot (cross-repo Composer auth)
PRIVATESATIS_BOT_PRIVATE_KEYALLGitHub App private key (PEM) for privatesatis-bot

Org variables (shared via secrets: inherit)

VariableVisibilityValuePurpose
PRIVATESATIS_USERNAMEALLgithub-middag-ioHTTP Basic username for privatesatis
CLOUDFLARE_ACCOUNT_IDALL32ff8929...Cloudflare account
DOCKERHUB_USERNAMEALLmiddagtecDocker Hub push username
SENTRY_ORGALLmiddagSentry organization slug
SMTP_HOSTPRIVATEemail-smtp.us-east-1...SES SMTP host
SMTP_PORTPRIVATE587SES SMTP port
SMTP_USERPRIVATEAKIA...SES SMTP IAM user

Feature flags — repo variables ("truques")

Repo-level variables act as feature flags to enable optional workflow steps without changing workflow code. Set them in GitHub repo Settings → Variables → Actions.

VariableScopeValuePurpose
PUSH_TO_ECRRepo vartrueEnable crane copy from GHCR to ECR after build (see ADR-007)
ECR_REPORepo varmy-projectECR repository name (required when PUSH_TO_ECR=true)

Pattern: any workflow step can be gated with if: vars.MY_FLAG == 'true'. This avoids per-repo workflow forks — one reusable workflow handles all repos, repo variables control behavior.


4. Docker Registry (GHCR / ECR)

Default: GHCR (GitHub Container Registry)

All Docker images push to GHCR by default:

ghcr.io/middag-io/{repo-name}/{service}:{tag}

Example:

ghcr.io/middag-io/docker-wp-my-project/wordpress:a1b2c3d4e5f6
ghcr.io/middag-io/docker-wp-my-project/nginx:latest

Optional: ECR (AWS Elastic Container Registry)

Enabled per-repo via repository variables:

PUSH_TO_ECR=true
ECR_REPO=my-project

When enabled, docker-build-push.yml uses crane copy to replicate from GHCR to ECR after build (no rebuild):

GHCR (build target) ──crane copy──> ECR (deploy source)

ECR credentials from 1Password vault CI-AWS.

When to use ECR

  • Production servers on AWS that benefit from same-region pull
  • Currently: only docker-wp-my-project needs ECR
  • Default for new projects: GHCR only unless specific need

5. Deploy to Production

Docker projects (EC2)

GitHub Actions                         EC2
     │                                  │
     ├── Build & push to GHCR           │
     ├── (optional) crane copy to ECR   │
     ├── SCP: docker-compose.yml,       │
     │        .env.production.tpl,      │
     │        Makefile                  │
     ├── SSH: docker compose pull       │
     │        op inject .env.tpl → .env │  ← 1Password Connect (local)
     │        docker compose up -d      │
     └── Cleanup                        │

WordPress plugin release

1. Merge PR from develop → main
2. release-please detects conventional commits on main
3. Creates/updates Release PR (version bump + CHANGELOG.md)
4. Merge Release PR → triggers:
   a. GitHub Release created (with tag)
   b. Post-release workflow: build UI → Strauss → ZIP → upload to Release
   c. Trigger privatesatis rebuild (repository_dispatch)
   d. Deploy docs to Cloudflare Pages (if applicable)

WordPress theme release

Same as plugin but version in style.css + functions.php.


6. Adding a New Project

Checklist

  1. Create repo following ADR-001:

    bash
    gh repo create middag-io/{name} --private --description "{desc}"
  2. Set default branch to main, create develop branch

  3. Add topics (platform, type, client)

  4. Add CI workflow calling reusable workflow:

    yaml
    jobs:
      ci:
        uses: middag-io/.github-private/.github/workflows/wp-plugin-ci.yml@workflows-v1
        with:
          has-tests: true
        secrets: inherit
  5. Add release-please config:

    • Create release-please-config.json with simple release type and extra-files for version header
    • Create .release-please-manifest.json with current version
    • Add // x-release-please-version annotations to version lines in main plugin/theme file
    • Add release workflow calling release-please.yml + wp-plugin-post-release.yml
  6. Configure secrets (if project needs its own SA):

    • Create Service Account in 1Password: sa-github-ci-{project}
    • Grant access to appropriate vaults
    • Add token as repo secret: OP_SA_{PROJECT}
  7. Set repo variables (vars.*) for workflow config

  8. Configure branch protection (or verify org ruleset applies)

  9. Test CI — push to develop, open PR, merge to main


7. Adding a New Secret

Decision tree

Is it shared across projects?
├── Yes → Add to CI-SHARED or CI-AWS vault
│         (already accessible via org SA)

└── No → Is there a project vault (CI-{PROJECT})?
         ├── Yes → Add to CI-{PROJECT}
         │         (already accessible via project SA)

         └── No → Create CI-{PROJECT} vault
                  Create sa-github-ci-{project} SA
                  Add OP_SA_{PROJECT} to repo secrets

After adding the secret

  1. Reference it in code: op://CI-{VAULT}/item-name/field-name
  2. If used in CI workflow: add to 1password/load-secrets-action env block
  3. If used on server: add to .env.production.tpl (Connect resolves automatically)
  4. Test in CI and/or staging before production

8. Legacy Migration from Bitbucket

Wave 1 (WordPress) is COMPLETE. The tables below remain useful reference for future migration waves.

Variable mapping

BitbucketGitHub Actions
$BITBUCKET_COMMIT${{ github.sha }}
$BITBUCKET_REPO_SLUG${{ github.event.repository.name }}
$BITBUCKET_REPO_FULL_NAME${{ github.repository }}
$BITBUCKET_BRANCH${{ github.ref_name }}
$BITBUCKET_TAG${{ github.ref_name }} (on tag trigger)
$BITBUCKET_SSH_KEY_FILEDeploy key or ssh-agent action
$BITBUCKET_BOT_USERNAMEgithub-actions[bot]
$BITBUCKET_BOT_PASSWORD${{ secrets.GITHUB_TOKEN }}

Pipe → Action mapping

Bitbucket PipeGitHub Equivalent
atlassian/bitbucket-upload-filegh release upload or actions/upload-artifact
atlassian/trigger-pipelinerepository_dispatch or workflow_dispatch

Pipeline → Workflow migration steps

  1. Read bitbucket-pipelines.yml
  2. Identify steps, triggers, conditions, services
  3. Map to GitHub Actions workflow using reusable workflows
  4. Replace BB variables with GH equivalents (table above)
  5. Replace BB pipes with GH actions (table above)
  6. Move BB pipeline variables to 1Password or GH repo variables
  7. Test on develop branch before merging to main

What is NOT migrated

  • Bitbucket Issues and Pull Requests (Jira used for tracking)
  • Bitbucket Downloads (move backups to S3 or GitHub Releases)
  • Pipeline build history (irrelevant)

Migration Status

WaveStatusNotes
0 — InfraCOMPLETE.github, CI workflows, org config
1 — WPCOMPLETE6 repos migrated, BB pipelines removed, reusable workflows + release-please
2 — LibsPendingRename + topics/properties polish
3 — AppsPending
4 — Moodle corePendingComplex CI
5 — Moodle sitesPendingPer-site migration

9. Troubleshooting

1Password in CI

ProblemCauseFix
could not find itemWrong vault or item name in op://Verify vault name + item title in 1Password
unauthorizedSA token doesn't have vault accessCheck SA permissions in 1Password web
connect: connection refusedConnect not running (infra only)docker compose up -d op-connect-api op-connect-sync

GitHub Actions

ProblemCauseFix
secrets: inherit not workingRepo not in org, or wrong workflow pathVerify repo under middag-io org
Reusable workflow not foundWrong ref or pathmiddag-io/.github-private/.github/workflows/{name}.yml@workflows-v1
Permission denied pushing tagBranch protection blocks botAdd bot to bypass list in ruleset

Docker

ProblemCauseFix
GHCR push deniedToken lacks write:packagesCheck GHCR_TOKEN scope in 1Password
ECR login failedAWS credentials expiredVerify CI-AWS vault credentials
crane copy failsAuth to target registryEnsure both registries logged in before copy

MIDDAG Tecnologia