From db47828dbce97dd99e579a850c244b2f36e75cf8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 5 Aug 2026 10:30:25 +0000 Subject: [PATCH 1/9] docs: add image transfer handover plan for Dev/QA Document how to hand over stage-to-stage image transfers from DevOps to Dev and QA with least privilege, approvals, audit, and stage-scoped workflows while keeping prod under Release/Security control. Co-authored-by: Ivanmeneges --- release/docs/image-transfer-handover-plan.md | 342 +++++++++++++++++++ release/vidivi/README.md | 2 + 2 files changed, 344 insertions(+) create mode 100644 release/docs/image-transfer-handover-plan.md diff --git a/release/docs/image-transfer-handover-plan.md b/release/docs/image-transfer-handover-plan.md new file mode 100644 index 00000000..0599f103 --- /dev/null +++ b/release/docs/image-transfer-handover-plan.md @@ -0,0 +1,342 @@ +# Image Transfer Handover Plan + +## Goal + +Hand over stage-to-stage Docker image transfer (`mosipdev → mosipdev2`, `mosipdev2 → mosipqa`, and similar paths) from DevOps to owning teams (Dev, QA), while keeping every transfer **authorized, audited, and hard to misuse**. + +This plan builds on the existing Vidivi tool and the [Manual workflow to transfer images](https://github.com/mosip/release-script/actions/workflows/image-transfer.yml). It does **not** remove DevOps ownership of production (`mosipid` / `mosipint`) or Security signing. + +--- + +## Problem today + +| Current state | Risk after naive handover | +|---|---| +| DevOps runs most transfers | Anyone with Actions access can overwrite tags | +| One workflow for all destinations | Wrong org / wrong tag / wrong version chaos | +| Secrets exist per org, but one shared workflow | Token selection mistakes; over-privilege | +| Ticket + PR practice is informal | Incomplete audit of who moved what and why | +| `mosipid` is admin-protected (via `kattu`) | Lower stages (`dev2`, `qa`) have weaker gates | + +Without controls, handover creates: accidental overwrites, tag pollution, environment skew (QA testing wrong digests), blame ambiguity, and pressure to “just re-run” without review. + +--- + +## Design principles + +1. **Least privilege** — A person or bot gets push rights only to the destination org(s) they own. +2. **Separation of duties** — Requester ≠ Approver ≠ (for prod) Releaser / Signer. +3. **Allowed paths only** — Transfers must follow the documented lifecycle; arbitrary org-to-org is blocked. +4. **PR is the request; workflow is the execution** — Image list changes are reviewed before push. +5. **Every run is attributable** — Actor, ticket, images, digests, destination, result are recorded. +6. **Fail closed** — Missing approval, wrong path, or mismatched secret → no transfer. +7. **DevOps remains the break-glass owner** — Emergency and prod stays with Release/DevOps + Security. + +--- + +## Image lifecycle (unchanged) + +``` +mosipdev → mosipdev2 → mosipqa → mosipid + ↘ mosipqa / mosipint → mosipid (staged / patch path) +``` + +Parallel Inji path (if used): `injistackdev → injistackdev2 → injistackqa → injistack`. + +| Org | Purpose | Post-handover owner | +|---|---|---| +| `mosipdev` | CI-built / early Dev | Dev (write via CI; not via transfer workflow) | +| `mosipdev2` | Pre-QA staging | Dev leads / designated Dev transfer operators | +| `mosipqa` | QA test images | QA leads / designated QA transfer operators | +| `mosipint` | Interim / patch holding | Release / DevOps (restricted) | +| `mosipid` | Community / production release | Release / DevOps + Security signing | + +--- + +## Target operating model + +### RACI (per stage) + +| Activity | Dev | QA | Release / DevOps | Security | +|---|---|---|---|---| +| Build & push to `mosipdev` (CI) | **R** | C | C | I | +| Transfer `mosipdev → mosipdev2` | **R** | C | A (policy) | I | +| Transfer `mosipdev2 → mosipqa` | C | **R** | A (policy) | I | +| Transfer `mosipqa → mosipid` / `mosipint` | C | C | **R** | **A** (signing) | +| Image signing after prod transfer | I | I | C | **R** | +| Break-glass / emergency transfer | C | C | **R** | A | +| Audit & access reviews | C | C | **R** | C | + +R = Responsible, A = Accountable, C = Consulted, I = Informed. + +### Named roles (not whole teams) + +Do **not** grant transfer rights to every Dev or QA engineer. + +| Role | Who | Rights | +|---|---|---| +| **Dev Transfer Operator** | 2–4 named Dev engineers (rotate) | Propose + (after approval) run transfers **into `mosipdev2` only** | +| **Dev Transfer Approver** | Dev lead(s) / module owners | Approve PRs / Environment for `→ mosipdev2` | +| **QA Transfer Operator** | 2–4 named QA engineers | Propose + run transfers **into `mosipqa` only** | +| **QA Transfer Approver** | QA lead(s) | Approve PRs / Environment for `→ mosipqa` | +| **Release Operator** | Build & Release / DevOps | Transfers into `mosipid` / `mosipint`; break-glass | +| **Security Signer** | Security team | Signing ticket after prod images land | +| **Audit reviewer** | Someone **not** who ran the transfer | Spot-check reports / digests (already aligned with post-release checks SoD) | + +--- + +## Allowed transfer matrix (hard rule) + +Only these destination hops are allowed for non-DevOps operators: + +| Source org (in `images.txt`) | Destination org | Who may execute | Approval required | +|---|---|---|---| +| `mosipdev` | `mosipdev2` | Dev Transfer Operator | Dev Approver | +| `mosipdev2` | `mosipqa` | QA Transfer Operator | QA Approver | +| `mosipqa` | `mosipid` | Release / DevOps (admin) | Release lead + Security signing | +| `mosipqa` / `mosipdev2` | `mosipint` | Release / DevOps (admin) | Release lead | +| Any → any other | — | **Blocked** | — | + +Additional rules: + +- Destination tag should match release/sprint convention (no free-form `latest` for MOSIP services unless explicitly allowed). +- Overwriting an existing tag in `mosipqa` / `mosipid` requires explicit approval comment (“overwrite intentional: reason”). +- Source image must exist; `check` mode must pass before `push`. +- Prefer digest verification (`hash`) when replacing a tag. + +--- + +## Control layers (defense in depth) + +Implement all layers. No single control is enough. + +### 1. People & process + +1. **Ticket required** — Jira/DSD ticket with: source org, dest org, version/tag, image list (or link to PR), reason, requester, target date. +2. **PR required** — Change `images.txt` (or a stage-specific list file) via PR. No direct push to protected branches. +3. **CODEOWNERS** — Stage-specific owners review image-list PRs. +4. **Runbook** — Operators follow a short checklist (below); free-form local `vidivi.py push` to shared orgs is discouraged / forbidden for operators without break-glass. + +### 2. GitHub repository access + +| Control | Recommendation | +|---|---| +| Who can run Actions | Restrict `workflow_dispatch` via GitHub Environments (not “all write collaborators”) | +| Branch protection | Require PR + approvals on the branch that holds image lists | +| CODEOWNERS | `/release/vidivi/images-dev2.txt` → Dev leads; `/release/vidivi/images-qa.txt` → QA leads | +| Admin list | Keep `mosipid` admin-only protection in `mosip/kattu` | + +### 3. Split workflows by stage (recommended) + +Replace “one mega-workflow anyone can aim at any org” with stage-scoped workflows: + +| Workflow | Destination fixed to | Secret | Environment | +|---|---|---|---| +| `image-transfer-dev2.yml` | `mosipdev2` only | `MOSIPDEV2_DOCKER_TOKEN` | `transfer-dev2` | +| `image-transfer-qa.yml` | `mosipqa` only | `MOSIPQA_DOCKER_TOKEN` | `transfer-qa` | +| `image-transfer-prod.yml` | `mosipid` / `mosipint` | `MOSIPID_*` / `MOSIPINT_*` | `transfer-prod` (admins + required reviewers) | + +Benefits: + +- Operators cannot “accidentally” select `MOSIPID_DOCKER_TOKEN`. +- Environment **required reviewers** gate the run. +- Audit logs clearly show which stage workflow ran. + +Keep the existing generic workflow for DevOps break-glass only, or remove it after migration. + +### 4. GitHub Environments (approval gates) + +For each environment (`transfer-dev2`, `transfer-qa`, `transfer-prod`): + +- **Required reviewers**: Approver role only (not the same GitHub group as Operators if possible). +- **Deployment branches**: Limit to agreed branches (e.g. `master` / release branch). +- **Wait timer** (optional for QA/prod): short delay so Slack notice can be seen before run proceeds. +- **Secrets scoped to Environment** — move Docker tokens from repo secrets to Environment secrets so only that workflow environment can use them. + +### 5. Registry credentials (strongest technical gate) + +| Destination | Credential type | Scope | Who holds it | +|---|---|---|---| +| `mosipdev2` | Docker Hub PAT / Harbor robot | **Push only to `mosipdev2`** | Environment `transfer-dev2` | +| `mosipqa` | Separate token/robot | **Push only to `mosipqa`** | Environment `transfer-qa` | +| `mosipid` / `mosipint` | Separate tokens | Push to those orgs only | Environment `transfer-prod` + admin | + +Rules: + +- Never share personal Docker passwords; use org tokens / Harbor robots with minimal scope. +- Rotate tokens on operator offboarding and on a fixed schedule (e.g. quarterly). +- Disable write access for tokens used only for CI pull if any. +- Prefer **immutable tags** or digest pinning in deploy configs so a mistaken re-push is less damaging (longer-term). + +### 6. Workflow / `kattu` enforcement + +Extend reusable workflow protections beyond `mosipid`: + +| Check | Behavior | +|---|---| +| Allowed destination list per workflow | Fail if input ≠ fixed dest org | +| Allowed source org prefix in `images.txt` | Fail if source not in allowlist for that hop | +| Actor allowlist (optional) | Fail if GitHub actor not in Operators team | +| Protected orgs | Keep admin-only for `mosipid` (and add `mosipint`) | +| Dry-run / check-first | Always run `check` (and optionally `hash`) before `push` | +| Block `custom` secret path for stage workflows | No custom secret on Dev/QA workflows | + +### 7. Monitoring & audit + +Every transfer must produce a durable record: + +| Signal | Where | Purpose | +|---|---|---| +| Workflow run | GitHub Actions run history | Who triggered, inputs, success/fail | +| Environment approval | GitHub Environment deployment log | Who approved | +| PR + ticket link | PR description / commit | Why | +| `transfer_report.md` | Committed or uploaded as workflow artifact | What moved | +| Digests | Report / `hash` output | Prove exact bits | +| Slack | Channel per stage (not only DevOps) | Real-time awareness | +| Weekly digest | Automated summary of transfers | Spot unusual volume / overwrites | + +**Minimum Slack payload:** actor, ticket/PR, source→dest, image count, success/fail, run URL. + +**Retention:** Keep Actions logs and transfer reports for at least one release cycle (prefer longer for prod). + +### 8. Verification & separation of duties + +- After `→ mosipqa`, a **QA engineer who did not run the transfer** confirms sample digests / smoke pull. +- After `→ mosipid`, continue existing Security signing ticket + post-release checks by a non-releaser (`release/checks`). + +--- + +## Standard operating procedure (SOP) + +### A. Dev → Dev2 (`mosipdev → mosipdev2`) + +1. Dev Transfer Operator opens / updates ticket: list of images + tags + reason. +2. Operator opens PR updating the Dev2 image list (e.g. `images-dev2.txt`). +3. Dev Approver reviews: correct sources, tags, no unintended overwrites. +4. PR merges. +5. Operator starts `image-transfer-dev2` workflow (branch with merged list). +6. Environment `transfer-dev2` requires Dev Approver approval. +7. Workflow runs `check` → `push`; publishes report + Slack to `#image-transfer-dev2` (or equivalent). +8. Operator links run URL + report back on the ticket; closes ticket. + +### B. Dev2 → QA (`mosipdev2 → mosipqa`) + +Same pattern with QA Operator / QA Approver / `image-transfer-qa` / `#image-transfer-qa`. + +**Entry criteria for QA handoff:** Dev confirms images validated in Dev2; ticket references sprint/release; tag set matches what QA will deploy. + +### C. QA → Prod (`mosipqa → mosipid`) + +Unchanged ownership: Release/DevOps only, admin-protected org, Security signing ticket after transfer, post-release checks by independent party. + +### D. Break-glass + +1. Only Release/DevOps. +2. Ticket marked `break-glass` with incident link. +3. Use prod or break-glass workflow; dual acknowledgment in Slack. +4. Post-incident: rotate credentials if compromise suspected; write short RCA. + +--- + +## Anti-chaos / anti-misuse rules + +| Rule | Why | +|---|---| +| Named operators only (small set) | Avoid “everyone can push” | +| Approver ≠ Operator for the same run | SoD | +| Fixed destination per workflow | No org mix-ups | +| No local push with shared org tokens | Tokens stay in CI Environments | +| One ticket / one PR / one hop | Prevent mega-batches mixing stages | +| Ban silent overwrites | Require explicit approval text | +| Rate / batch expectations | Unusual large transfers need lead approval | +| Freeze windows | Optional: no QA transfers during freeze without Release approval | +| Offboarding checklist | Remove from GitHub team + rotate token same day | + +--- + +## Implementation roadmap + +### Phase 0 — Agree ownership (no code) + +- Nominate Operators and Approvers for Dev and QA. +- Confirm Slack channels and ticket project/labels (`image-transfer-dev2`, `image-transfer-qa`). +- Document freeze/break-glass contacts. + +### Phase 1 — Process without new workflows + +- Enforce ticket + PR for all lower-stage transfers. +- Add CODEOWNERS on `release/vidivi/images.txt` (or split files). +- DevOps still executes until Phase 2; teams prepare lists themselves. +- Start Slack notification habit and ticket linkage. + +### Phase 2 — Technical gates (recommended core) + +- Split workflows: `dev2`, `qa`, `prod`. +- Create GitHub Environments with required reviewers + environment-scoped secrets. +- Move tokens off shared repo-secret usage where possible. +- Restrict Environment access to Operator GitHub teams. +- Tighten `kattu` allowlists (source org + destination org). +- Keep generic workflow admin-only or retire it. + +### Phase 3 — Observability & hygiene + +- Structured Slack notifications per stage. +- Archive transfer reports as artifacts + optional commit. +- Weekly transfer summary (Actions API or script). +- Token rotation calendar; access review every sprint or monthly. +- Optional: immutable tags / digest pinning in Helm values for QA/prod. + +### Phase 4 — Continuous improvement + +- Metrics: failed transfers, overwrite rate, time-to-approve, transfers without ticket. +- Periodic drill: revoke an operator; confirm they cannot approve or run. +- Align Inji / other org paths to the same model. + +--- + +## Suggested GitHub team layout + +| GitHub team | Members | Used for | +|---|---|---| +| `mosip-image-transfer-dev-ops` | Dev Operators | Can start `transfer-dev2` | +| `mosip-image-transfer-dev-approvers` | Dev Approvers | Environment reviewers for `transfer-dev2` | +| `mosip-image-transfer-qa-ops` | QA Operators | Can start `transfer-qa` | +| `mosip-image-transfer-qa-approvers` | QA Approvers | Environment reviewers for `transfer-qa` | +| `mosip-release-admins` | Release/DevOps | `transfer-prod` + break-glass | + +Prefer **disjoint** ops vs approver teams so the same person is not always self-approving. If headcount is small, allow self-approve only for Dev2, never for QA→Prod; still keep a second person review on the PR. + +--- + +## Checklist before declaring handover complete + +- [ ] Operators and Approvers named and documented +- [ ] Stage workflows live with fixed destinations +- [ ] Environment required reviewers configured +- [ ] Tokens scoped per org and stored as Environment secrets +- [ ] CODEOWNERS / PR review on image lists +- [ ] Slack alerts working per stage +- [ ] SOP published; teams trained on one dry-run each +- [ ] `mosipid` / `mosipint` still admin-only; Security signing unchanged +- [ ] Break-glass procedure written and tested once +- [ ] Offboarding + token rotation procedure owned by DevOps +- [ ] First two real transfers audited end-to-end (ticket → PR → approve → run → report) + +--- + +## What DevOps still owns after handover + +- Production / interim transfers (`mosipid`, `mosipint`) +- Reusable workflow protections in `mosip/kattu` +- Registry org policy, token issuance/rotation +- Break-glass execution +- Access reviews and audit tooling +- Platform changes to Vidivi / Actions + +Dev and QA own **their hop only**: preparing the image list, getting approval, triggering the stage workflow, and confirming the result for that stage. + +--- + +## Summary + +Hand over **authority by stage**, not the whole transfer button. Combine **named operators**, **mandatory ticket+PR**, **stage-specific workflows**, **GitHub Environment approvals**, **org-scoped registry tokens**, and **Slack/report audit**. Keep **prod and signing** with Release/DevOps + Security. That is the clean path to remove DevOps from day-to-day `dev→dev2` and `dev2→qa` moves without inviting misuse or chaos. diff --git a/release/vidivi/README.md b/release/vidivi/README.md index dc60d521..26d933b7 100644 --- a/release/vidivi/README.md +++ b/release/vidivi/README.md @@ -4,6 +4,8 @@ ## Overview +For handing stage transfers (`mosipdev` → `mosipdev2`, `mosipdev2` → `mosipqa`) from DevOps to Dev/QA with access control, approvals, and monitoring, see **[Image Transfer Handover Plan](../docs/image-transfer-handover-plan.md)**. + In MOSIP, we maintain several Docker Hub organizations with specific purposes: * **`mosipid`**: Contains officially released Docker images for the Open Source Community From 813f6aab125d444b5c70672f6887a85287ac7438 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 5 Aug 2026 11:49:02 +0000 Subject: [PATCH 2/9] docs: explain GitHub Environments and add stage transfer workflows Add a full how-to for GitHub Environments (approval gates + env secrets) and stage-scoped workflows for mosipdev2 and mosipqa transfers. Co-authored-by: Ivanmeneges --- .github/workflows/image-transfer-dev2.yml | 49 +++ .github/workflows/image-transfer-qa.yml | 49 +++ .../github-environments-image-transfer.md | 371 ++++++++++++++++++ 3 files changed, 469 insertions(+) create mode 100644 .github/workflows/image-transfer-dev2.yml create mode 100644 .github/workflows/image-transfer-qa.yml create mode 100644 release/docs/github-environments-image-transfer.md diff --git a/.github/workflows/image-transfer-dev2.yml b/.github/workflows/image-transfer-dev2.yml new file mode 100644 index 00000000..906f86e6 --- /dev/null +++ b/.github/workflows/image-transfer-dev2.yml @@ -0,0 +1,49 @@ +name: Transfer images to mosipdev2 + +# Stage-scoped transfer: destination is fixed to mosipdev2. +# Requires GitHub Environment "transfer-dev2" with required reviewers +# and Environment secret MOSIPDEV2_DOCKER_TOKEN. +# See: release/docs/github-environments-image-transfer.md + +on: + workflow_dispatch: + inputs: + USERNAME: + description: 'Registry username (Docker Hub user or Harbor robot)' + required: true + default: '' + type: string + REGISTRY_URL: + description: 'Destination registry URL' + required: true + default: 'https://index.docker.io/v1/' + type: string + REGISTRY_TYPE: + description: 'Destination registry type' + required: true + default: 'dockerhub' + type: choice + options: + - dockerhub + - harbor + - other + ENABLE_WIREGUARD: + description: 'Enable WireGuard VPN (required for private Harbor networks)' + required: false + default: false + type: boolean + +jobs: + Image-transfer: + environment: transfer-dev2 + uses: mosip/kattu/.github/workflows/image-transfer.yml@master + with: + DESTINATION_ORGANIZATION: mosipdev2 + REGISTRY_URL: ${{ inputs.REGISTRY_URL }} + REGISTRY_TYPE: ${{ inputs.REGISTRY_TYPE }} + ENABLE_WIREGUARD: ${{ inputs.ENABLE_WIREGUARD }} + USERNAME: ${{ inputs.USERNAME }} + secrets: + TOKEN: ${{ secrets.MOSIPDEV2_DOCKER_TOKEN }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_DEVOPS }} + WIREGUARD_CONFIG: ${{ secrets.WIREGUARD_CONFIG }} diff --git a/.github/workflows/image-transfer-qa.yml b/.github/workflows/image-transfer-qa.yml new file mode 100644 index 00000000..5e64dd8b --- /dev/null +++ b/.github/workflows/image-transfer-qa.yml @@ -0,0 +1,49 @@ +name: Transfer images to mosipqa + +# Stage-scoped transfer: destination is fixed to mosipqa. +# Requires GitHub Environment "transfer-qa" with required reviewers +# and Environment secret MOSIPQA_DOCKER_TOKEN. +# See: release/docs/github-environments-image-transfer.md + +on: + workflow_dispatch: + inputs: + USERNAME: + description: 'Registry username (Docker Hub user or Harbor robot)' + required: true + default: '' + type: string + REGISTRY_URL: + description: 'Destination registry URL' + required: true + default: 'https://index.docker.io/v1/' + type: string + REGISTRY_TYPE: + description: 'Destination registry type' + required: true + default: 'dockerhub' + type: choice + options: + - dockerhub + - harbor + - other + ENABLE_WIREGUARD: + description: 'Enable WireGuard VPN (required for private Harbor networks)' + required: false + default: false + type: boolean + +jobs: + Image-transfer: + environment: transfer-qa + uses: mosip/kattu/.github/workflows/image-transfer.yml@master + with: + DESTINATION_ORGANIZATION: mosipqa + REGISTRY_URL: ${{ inputs.REGISTRY_URL }} + REGISTRY_TYPE: ${{ inputs.REGISTRY_TYPE }} + ENABLE_WIREGUARD: ${{ inputs.ENABLE_WIREGUARD }} + USERNAME: ${{ inputs.USERNAME }} + secrets: + TOKEN: ${{ secrets.MOSIPQA_DOCKER_TOKEN }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_DEVOPS }} + WIREGUARD_CONFIG: ${{ secrets.WIREGUARD_CONFIG }} diff --git a/release/docs/github-environments-image-transfer.md b/release/docs/github-environments-image-transfer.md new file mode 100644 index 00000000..ad5af88c --- /dev/null +++ b/release/docs/github-environments-image-transfer.md @@ -0,0 +1,371 @@ +# GitHub Environments for Image Transfer — What They Are & How to Implement + +## What is a GitHub Environment? + +A **GitHub Environment** is a named deployment target in a repository (for example `transfer-dev2`, `transfer-qa`, `transfer-prod`). It is **not** a server and **not** Docker Hub. It is a **control gate** inside GitHub Actions. + +When a workflow job declares: + +```yaml +jobs: + transfer: + environment: transfer-dev2 +``` + +GitHub does three important things **before that job can run or read environment secrets**: + +1. Applies **protection rules** (required reviewers, wait timer, allowed branches). +2. Creates a **deployment record** (who triggered, who approved, when, which commit). +3. Unlocks **Environment secrets** only for that job (and only after rules pass). + +### Why this matters for image transfer + +Today, anyone who can run the manual image-transfer workflow can aim it at any org (if they pick the right secret). With Environments: + +| Without Environments | With Environments | +|---|---| +| Click **Run workflow** → job starts immediately | Click **Run workflow** → job waits for Approver | +| Repo secrets readable by any write-access workflow | Docker push token locked in Environment; released only after approval | +| Weak audit of “who allowed this” | Approver name stored on the deployment | +| One workflow can target any destination | Stage workflow + fixed Environment = fixed destination | + +``` +Operator clicks "Run workflow" + │ + ▼ +Job references environment: transfer-qa + │ + ▼ +GitHub pauses job ──► Slack/email to Required Reviewers + │ + ▼ +QA Approver clicks Approve (or Reject) + │ + ▼ +Environment secrets unlocked (e.g. MOSIPQA_DOCKER_TOKEN) + │ + ▼ +Reusable kattu image-transfer job runs + │ + ▼ +Deployment history + Actions log = audit trail +``` + +### Requirements / limits to know + +- **Required reviewers** and **environment secrets** on private repos need GitHub Team / Enterprise (public repos: available on Free). MOSIP public repos can use this; confirm plan if the repo is private/internal. +- Up to **6** required reviewers (users or teams). **Any one** approval is enough (not majority). +- Enable **Prevent self-review** so the person who started the run cannot approve it. +- Unapproved jobs fail after **30 days**. +- Environment secrets are only visible to jobs that set `environment: `. + +--- + +## Target setup for MOSIP image transfer + +| Environment name | Used by workflow | Required reviewers | Environment secret(s) | Fixed destination org | +|---|---|---|---|---| +| `transfer-dev2` | `image-transfer-dev2.yml` | Dev Approver team | `MOSIPDEV2_DOCKER_TOKEN` | `mosipdev2` | +| `transfer-qa` | `image-transfer-qa.yml` | QA Approver team | `MOSIPQA_DOCKER_TOKEN` | `mosipqa` | +| `transfer-prod` | `image-transfer-prod.yml` | Release admins | `MOSIPID_DOCKER_TOKEN`, `MOSIPINT_DOCKER_TOKEN` | `mosipid` / `mosipint` | + +Repo-level secrets that can stay shared (not stage-specific): `SLACK_WEBHOOK_DEVOPS`, `WIREGUARD_CONFIG`. + +--- + +## Full implementation steps + +### Phase A — Prepare people (Day 0) + +#### Step A1. Nominate roles + +| Role | Example | Count | +|---|---|---| +| Dev Transfer Operators | Engineers who prepare PRs and click Run | 2–4 | +| Dev Transfer Approvers | Dev leads who approve Environment waits | 1–3 | +| QA Transfer Operators | QA engineers | 2–4 | +| QA Transfer Approvers | QA leads | 1–3 | +| Release admins | DevOps / Release | existing | + +Approvers and Operators should be **different people** when possible. + +#### Step A2. Create GitHub teams (org settings) + +Path: GitHub **Organization** → **Teams** → **New team** + +Create: + +1. `mosip-image-transfer-dev-ops` +2. `mosip-image-transfer-dev-approvers` +3. `mosip-image-transfer-qa-ops` +4. `mosip-image-transfer-qa-approvers` +5. `mosip-release-admins` (if not already present) + +Add the named people to each team. + +#### Step A3. Grant repo access + +Path: `mosip/release-script` → **Settings** → **Collaborators and teams** + +- Give Operator teams **Write** (needed to run `workflow_dispatch`). +- Approver teams need at least **Read** (GitHub allows Environment approval with read); Write is fine if they already have it. +- Do **not** put all of Dev/QA on Write just for this — only named operators. + +--- + +### Phase B — Create Environments in the repo (UI) + +Path: `mosip/release-script` → **Settings** → **Environments** → **New environment** + +Repeat for `transfer-dev2`, `transfer-qa`, `transfer-prod`. + +#### Step B1. Create environment `transfer-dev2` + +1. Name: `transfer-dev2` +2. Click **Configure environment**. + +#### Step B2. Protection rules + +1. Check **Required reviewers**. +2. Add team: `mosip-image-transfer-dev-approvers` (or individual leads). +3. Check **Prevent self-review**. +4. Optional: **Wait timer** = `1`–`5` minutes (gives Slack time to notify). +5. Optional: **Deployment branches** → Selected branches → allow only `master` / `main` / your release branch (stops runs from random forks/branches). +6. Click **Save protection rules**. + +#### Step B3. Environment secret + +1. Under **Environment secrets** → **Add environment secret**. +2. Name: `MOSIPDEV2_DOCKER_TOKEN` +3. Value: Docker Hub / Harbor token that can **push only to `mosipdev2`**. +4. Save. + +#### Step B4. Repeat for QA and Prod + +| Environment | Reviewers | Prevent self-review | Secrets | +|---|---|---|---| +| `transfer-qa` | `mosip-image-transfer-qa-approvers` | Yes | `MOSIPQA_DOCKER_TOKEN` | +| `transfer-prod` | `mosip-release-admins` | Yes | `MOSIPID_DOCKER_TOKEN`, `MOSIPINT_DOCKER_TOKEN` | + +#### Step B5. Move tokens off repo secrets (important) + +After Environment secrets work in a test run: + +1. Settings → **Secrets and variables** → **Actions** (repository secrets). +2. Remove or rotate the old repo-level `MOSIPDEV2_DOCKER_TOKEN` / `MOSIPQA_DOCKER_TOKEN` so they are **not** usable by unprotected workflows. +3. Keep `SLACK_WEBHOOK_DEVOPS` and `WIREGUARD_CONFIG` as repository secrets if all workflows need them. + +Until you remove repo copies, a workflow **without** an Environment can still use the old repo secret — that defeats the gate. + +--- + +### Phase C — Add stage workflows (code) + +Add separate workflows so destination cannot be mistyped. + +#### Example: Dev2 workflow + +File: `.github/workflows/image-transfer-dev2.yml` + +```yaml +name: Transfer images to mosipdev2 + +on: + workflow_dispatch: + inputs: + USERNAME: + description: 'Registry username (Docker Hub user or Harbor robot)' + required: true + type: string + REGISTRY_URL: + description: 'Destination registry URL' + required: true + default: 'https://index.docker.io/v1/' + type: string + REGISTRY_TYPE: + description: 'Destination registry type' + required: true + default: 'dockerhub' + type: choice + options: + - dockerhub + - harbor + - other + ENABLE_WIREGUARD: + description: 'Enable WireGuard VPN for private Harbor' + required: false + default: false + type: boolean + +jobs: + # Gate: waits for Environment approval; unlocks Environment secrets + Image-transfer: + environment: transfer-dev2 + uses: mosip/kattu/.github/workflows/image-transfer.yml@master + with: + DESTINATION_ORGANIZATION: mosipdev2 # FIXED — operators cannot change this + REGISTRY_URL: ${{ inputs.REGISTRY_URL }} + REGISTRY_TYPE: ${{ inputs.REGISTRY_TYPE }} + ENABLE_WIREGUARD: ${{ inputs.ENABLE_WIREGUARD }} + USERNAME: ${{ inputs.USERNAME }} + secrets: + # From Environment secret (available only after approval) + TOKEN: ${{ secrets.MOSIPDEV2_DOCKER_TOKEN }} + # From repository secrets + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_DEVOPS }} + WIREGUARD_CONFIG: ${{ secrets.WIREGUARD_CONFIG }} +``` + +#### Example: QA workflow + +File: `.github/workflows/image-transfer-qa.yml` + +Same shape, but: + +- `environment: transfer-qa` +- `DESTINATION_ORGANIZATION: mosipqa` +- `TOKEN: ${{ secrets.MOSIPQA_DOCKER_TOKEN }}` + +#### Example: Prod workflow (DevOps only) + +- `environment: transfer-prod` +- Input choice limited to `mosipid` / `mosipint` only +- Tokens from prod Environment secrets +- Optionally restrict who can see/run it via repo permissions + admin-only kattu protection + +#### What to do with the old generic workflow + +| Option | Recommendation | +|---|---| +| A. Restrict to admins / leave as break-glass | Short term OK | +| B. Delete after stage workflows are proven | Preferred long term | +| C. Point it at `transfer-prod` only | If you still need one flexible entry for Release | + +Do **not** leave the old workflow able to read Dev2/QA tokens from repository secrets. + +--- + +### Phase D — Image list process (PR gate) + +Environments gate **execution**. PRs gate **what** gets transferred. + +1. Operator updates `release/vidivi/images.txt` (or split files `images-dev2.txt` / `images-qa.txt`) via PR. +2. Approver reviews image list + ticket link. +3. Merge PR. +4. Operator runs the stage workflow on the branch that contains the merged list. +5. Environment Approver approves the waiting deployment. + +Optional CODEOWNERS: + +``` +# .github/CODEOWNERS +/release/vidivi/images-dev2.txt @mosip/mosip-image-transfer-dev-approvers +/release/vidivi/images-qa.txt @mosip/mosip-image-transfer-qa-approvers +``` + +--- + +### Phase E — Operator & Approver runbook + +#### Operator (start transfer) + +1. Ensure PR with image list is merged and ticket is open. +2. Go to **Actions** → **Transfer images to mosipdev2** (or QA). +3. **Run workflow** → fill username / registry → Run. +4. Job shows status **Waiting** (yellow) for Environment approval. +5. Notify Approver (Slack) with Actions run URL + ticket. +6. After approval, wait for success; attach `transfer_report` / run URL on ticket. + +#### Approver (approve or reject) + +1. Open the Actions run URL (or repo **Deployments**). +2. Review: actor, branch, destination Environment, linked ticket/PR. +3. Click **Review deployments** → select Environment → **Approve** or **Reject**. +4. If something looks wrong (wrong branch, no ticket, unexpected overwrite): **Reject** and comment on ticket. + +Screenshot path in UI: + +`Actions` → select run → banner **Review deployments** → choose environment → Approve / Reject. + +--- + +### Phase F — Verification checklist (do this once before handover) + +- [ ] Create Environments with required reviewers + prevent self-review. +- [ ] Put Docker tokens only in Environment secrets; remove from repo secrets. +- [ ] Merge stage workflow files (`*-dev2`, `*-qa`, `*-prod`). +- [ ] Test Dev2: Operator starts run → job waits → Approver approves → transfer succeeds. +- [ ] Test self-review blocked: Operator who started run cannot approve. +- [ ] Test reject path: Approver rejects → job does not transfer. +- [ ] Confirm old generic workflow cannot push to Dev2/QA (token gone or workflow restricted). +- [ ] Confirm Slack still notifies. +- [ ] Confirm Deployment history shows actor + approver. +- [ ] Train both teams with one dry-run each. + +--- + +## How secrets resolve (important detail) + +```yaml +secrets: + TOKEN: ${{ secrets.MOSIPDEV2_DOCKER_TOKEN }} +``` + +Lookup order for a job with `environment: transfer-dev2`: + +1. Environment secret `MOSIPDEV2_DOCKER_TOKEN` on `transfer-dev2` (preferred). +2. Else repository / org secret with the same name (fallback — avoid leaving these). + +Only jobs that declare that Environment get Environment secrets, and only **after** protection rules pass. That is the core security property. + +--- + +## Mapping to reusable workflow (`mosip/kattu`) + +Your caller already does: + +```yaml +uses: mosip/kattu/.github/workflows/image-transfer.yml@master +``` + +Putting `environment:` on that **caller** job is enough: + +- GitHub waits for approval **before** starting the reusable workflow. +- Caller can pass Environment secrets into `secrets: TOKEN: ...`. +- Existing `mosipid` admin protection inside `kattu` remains an extra layer for prod. + +No change is required inside `kattu` for basic Environment gating. Optional later: add source-org allowlists inside `kattu` for defense in depth. + +--- + +## Minimal vs full adoption + +| Level | What you do | Protection gained | +|---|---|---| +| **Minimal** | One Environment on existing workflow + required reviewers | Human approval before any transfer | +| **Recommended** | Stage Environments + stage workflows + Environment secrets | Approval + least-privilege tokens + fixed destination | +| **Full** | Above + CODEOWNERS + ticket SOP + remove generic workflow + weekly access review | Process + technical + audit hygiene | + +Start with **Recommended** for `transfer-dev2` and `transfer-qa`; keep prod on `transfer-prod` with Release only. + +--- + +## Troubleshooting + +| Symptom | Likely cause | Fix | +|---|---|---| +| Job never waits for approval | Job missing `environment:` | Add `environment: transfer-dev2` on the transfer job | +| `TOKEN` empty / unauthorized | Secret still only on repo, or wrong name; or job has no environment | Add Environment secret; ensure job references that Environment | +| Operator can approve own run | Prevent self-review off | Enable on Environment | +| Approver cannot see Approve button | Not in required reviewers / no repo access | Add to reviewer team; grant Read+ | +| Wrong images transferred | `images.txt` not reviewed | Enforce PR + CODEOWNERS before run | +| Old workflow still pushes | Repo secret still present | Delete/rotate repo secret | + +--- + +## Related docs + +- [Image Transfer Handover Plan](./image-transfer-handover-plan.md) +- [Vidivi README](../vidivi/README.md) +- GitHub docs: [Managing environments](https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments) +- GitHub docs: [Reviewing deployments](https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/review-deployments) From 79b0174a49cc117f145e61580ba7e2e5dc29483c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 5 Aug 2026 11:49:17 +0000 Subject: [PATCH 3/9] docs: link Environments how-to from handover plan and Vidivi README Co-authored-by: Ivanmeneges --- release/docs/image-transfer-handover-plan.md | 2 ++ release/vidivi/README.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/release/docs/image-transfer-handover-plan.md b/release/docs/image-transfer-handover-plan.md index 0599f103..739cbc6e 100644 --- a/release/docs/image-transfer-handover-plan.md +++ b/release/docs/image-transfer-handover-plan.md @@ -153,6 +153,8 @@ For each environment (`transfer-dev2`, `transfer-qa`, `transfer-prod`): - **Wait timer** (optional for QA/prod): short delay so Slack notice can be seen before run proceeds. - **Secrets scoped to Environment** — move Docker tokens from repo secrets to Environment secrets so only that workflow environment can use them. +**Full explanation and UI/YAML steps:** [GitHub Environments for Image Transfer](./github-environments-image-transfer.md). + ### 5. Registry credentials (strongest technical gate) | Destination | Credential type | Scope | Who holds it | diff --git a/release/vidivi/README.md b/release/vidivi/README.md index 26d933b7..92e776ec 100644 --- a/release/vidivi/README.md +++ b/release/vidivi/README.md @@ -4,7 +4,7 @@ ## Overview -For handing stage transfers (`mosipdev` → `mosipdev2`, `mosipdev2` → `mosipqa`) from DevOps to Dev/QA with access control, approvals, and monitoring, see **[Image Transfer Handover Plan](../docs/image-transfer-handover-plan.md)**. +For handing stage transfers (`mosipdev` → `mosipdev2`, `mosipdev2` → `mosipqa`) from DevOps to Dev/QA with access control, approvals, and monitoring, see **[Image Transfer Handover Plan](../docs/image-transfer-handover-plan.md)** and **[GitHub Environments setup (full steps)](../docs/github-environments-image-transfer.md)**. In MOSIP, we maintain several Docker Hub organizations with specific purposes: From e7a72e595250093f4a301e729d4fb067ab80f188 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 10 Aug 2026 06:33:09 +0000 Subject: [PATCH 4/9] docs: guide testing Environment approval on WG onboard/offboard Explain how to add environment: wg-lifecycle to mosip/infra's WireGuard workflow as a safe first dry-run of required-reviewer approvals. Co-authored-by: Ivanmeneges --- release/docs/wg-lifecycle-approval-test.md | 129 +++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 release/docs/wg-lifecycle-approval-test.md diff --git a/release/docs/wg-lifecycle-approval-test.md b/release/docs/wg-lifecycle-approval-test.md new file mode 100644 index 00000000..7c870266 --- /dev/null +++ b/release/docs/wg-lifecycle-approval-test.md @@ -0,0 +1,129 @@ +# Test GitHub Environment approval on WireGuard onboard/offboard + +Use the rapid-deployment **WireGuard onboard/offboard** workflow in +[`mosip/infra`](https://github.com/mosip/infra/blob/master/.github/workflows/wg-onboard.yml) +as the first place to try Approve / Reject — before rolling the same pattern +to image transfer. + +## Important design rule + +Do **not** set: + +```yaml +environment: ${{ inputs.ENV_NAME }} +``` + +That input is the **target** Environment this workflow creates (Helmsman secrets +`TF_WG_CONFIG`, `CLUSTER_WIREGUARD_WG0`, `CLUSTER_WIREGUARD_WG1`). On first +onboard it often does not exist yet, so it cannot be the approval gate. + +Use a **fixed gate** Environment instead: + +```yaml +jobs: + onboard: + environment: wg-lifecycle # approval gate (fixed name) +``` + +| Name | Role | +|---|---| +| `wg-lifecycle` | Who must Approve before the job runs | +| `inputs.ENV_NAME` (e.g. `qa-foo`) | Where WG peer secrets are written | + +## Step 1 — Create Environment `wg-lifecycle` (UI, ~2 minutes) + +In **https://github.com/mosip/infra**: + +1. **Settings** → **Environments** → **New environment** +2. Name: `wg-lifecycle` (exact spelling) +3. Enable **Required reviewers** → add yourself + one other person (or a team) +4. Enable **Prevent self-review** +5. Optional: wait timer `1` minute; limit deployment branches to `master` +6. **Save protection rules** + +You do **not** need to move secrets yet for the first UI test. + +## Step 2 — One-line workflow change + +In `.github/workflows/wg-onboard.yml`, under `jobs.onboard`, add: + +```yaml +jobs: + onboard: + runs-on: [self-hosted, Linux, X64] + environment: wg-lifecycle # ← add this line + timeout-minutes: 20 + steps: + # ... existing steps unchanged ... +``` + +Commit on a branch and merge (or test from that branch via **Run workflow** → select the branch). + +Full proposed file comment header: + +```yaml +# Approval gate: job uses GitHub Environment "wg-lifecycle" (required reviewers). +# That gate is SEPARATE from the target ENV_NAME environment this workflow creates. +``` + +## Step 3 — Safe test run (DRY_RUN = true) + +1. **Actions** → **WireGuard environment onboard/offboard** → **Run workflow** +2. Use: + +| Input | Value | +|---|---| +| Branch | branch that contains `environment: wg-lifecycle` | +| `ACTION` | `onboard` | +| `ENV_NAME` | e.g. `wg-approval-test` | +| `JUMPSERVER_HOST` | your usual jumpserver | +| `TICKET` | `TEST-APPROVAL` | +| `DRY_RUN` | **true** (default) | + +3. Open the run → job should be **Waiting** on `wg-lifecycle` +4. As **Approver** (not the person who clicked Run): **Review deployments** → Approve +5. Job continues; dry-run logs show planned actions without writing peers/secrets +6. Repeat once with **Reject** to confirm the deny path + +## Step 4 — What you should see + +| Checkpoint | Expected | +|---|---| +| Before approve | Job status Waiting; no SSH/script steps yet | +| After approve | Steps run; deployment shows actor + approver | +| Self-review | Operator cannot approve own run | +| Reject | Job does not perform onboard | +| DRY_RUN=true | No secret/`assigned.txt` writes | + +## Step 5 — Optional hardening (after UX works) + +1. Copy `ACTION_PAT` and `MOSIP_AWS_PEM` to **Environment secrets** on `wg-lifecycle` +2. Remove them from repository secrets (so only approved jobs can use them) +3. Only then use `DRY_RUN=false` for a real onboard with a real ticket + +## Day-to-day after rollout + +| Who | Does | +|---|---| +| QA/Dev operator | Runs workflow with ticket + ENV_NAME | +| DevOps / lead approver | Approves or rejects waiting deployment | +| Same person for both | Blocked if Prevent self-review is on | + +## Same pattern for image transfer later + +| Action | Gate Environment | Fixed destination | +|---|---|---| +| WG onboard/offboard | `wg-lifecycle` | N/A (target is `ENV_NAME` input) | +| Images → mosipdev2 | `transfer-dev2` | `mosipdev2` | +| Images → mosipqa | `transfer-qa` | `mosipqa` | + +WireGuard is the better first test because **dry-run is already the default**. + +## Troubleshooting + +| Symptom | Cause | Fix | +|---|---|---| +| No Waiting state | `environment:` missing on that branch | Merge/select correct branch | +| Waiting but no reviewers notified | Required reviewers not saved | Re-open Environment settings | +| Operator can approve self | Prevent self-review off | Enable it | +| Job waits forever | Approver not in list / no access | Add reviewer; grant at least Read on repo | From a309b6b8cb900a68bf35de6ea93c214f53a43ac8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 10 Aug 2026 06:33:18 +0000 Subject: [PATCH 5/9] docs: link WG approval test from Environments how-to Co-authored-by: Ivanmeneges --- release/docs/github-environments-image-transfer.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release/docs/github-environments-image-transfer.md b/release/docs/github-environments-image-transfer.md index ad5af88c..5886d534 100644 --- a/release/docs/github-environments-image-transfer.md +++ b/release/docs/github-environments-image-transfer.md @@ -366,6 +366,7 @@ Start with **Recommended** for `transfer-dev2` and `transfer-qa`; keep prod on ` ## Related docs - [Image Transfer Handover Plan](./image-transfer-handover-plan.md) +- [Test approval on WireGuard onboard/offboard (recommended first)](./wg-lifecycle-approval-test.md) - [Vidivi README](../vidivi/README.md) - GitHub docs: [Managing environments](https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments) - GitHub docs: [Reviewing deployments](https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/review-deployments) From 077bf22a37bbbe96c8a08e8a4b2d229bfd781960 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 3 Sep 2026 03:47:21 +0000 Subject: [PATCH 6/9] docs: add image transfer KT with scenario examples from merged PRs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document the as-is MOSIP/Inji image transfer process end-to-end, including images.txt format, workflow cheat sheet, and real PR scenarios (dev→dev2, dev2→qa, qa→int, mosipid, Inji). Co-authored-by: Ivanmeneges --- release/docs/image-transfer-kt.md | 455 ++++++++++++++++++++++++++++++ 1 file changed, 455 insertions(+) create mode 100644 release/docs/image-transfer-kt.md diff --git a/release/docs/image-transfer-kt.md b/release/docs/image-transfer-kt.md new file mode 100644 index 00000000..6538ff8f --- /dev/null +++ b/release/docs/image-transfer-kt.md @@ -0,0 +1,455 @@ +# Knowledge Transfer: Current Image Transfer Process + +**Audience:** DevOps, Dev, QA, Release engineers who need to understand or execute Docker image transfers today +**Repo:** [`mosip/release-script`](https://github.com/mosip/release-script) +**Tooling:** Vidivi (`release/vidivi/`) + GitHub Actions workflow `image-transfer.yml` +**Status:** This document describes the **present** (as-is) process. Future handover / Environment approval controls are separate docs. + +--- + +## 1. What problem does image transfer solve? + +CI builds push images into a **development** Docker Hub org (for example `mosipdev` or `injistackdev`). Other stages (pre-QA, QA, interim, production) use **different orgs**. We do **not** rebuild the image for each stage; we **copy** the same image (via Crane) from source org/tag to destination org/tag. + +That copy is what we call **image transfer**. + +``` +Build (CI) ──push──► mosipdev ──transfer──► mosipdev2 ──transfer──► mosipqa ──transfer──► mosipid + │ │ + │ └──► mosipint (optional / patches) + pre-QA staging QA testing +``` + +--- + +## 2. Docker Hub organizations (MOSIP + Inji) + +### MOSIP path + +| Org | Purpose | Typical who asks for transfer | +|---|---|---| +| `mosipdev` | CI-built / early Dev images | — (created by build pipelines) | +| `mosipdev2` | Pre-QA staging / stabilization | Dev / module owners | +| `mosipqa` | Images for QA test environments | Dev / QA after Dev2 validation | +| `mosipint` | Interim / implementation / patch holding | Release / DevOps | +| `mosipid` | Community / production release images | Release / DevOps (+ Security signing after) | + +### Inji parallel path (same idea, different org names) + +| Org | Role | +|---|---| +| `injistackdev` | Dev | +| `injistackdev2` | Pre-QA | +| `injistackqa` | QA | +| `injistack` | Released / production-equivalent | + +Lifecycle (recommended): + +``` +mosipdev → mosipdev2 → mosipqa → mosipid +``` + +Staged / patch path: + +``` +mosipdev → mosipdev2 → mosipqa → mosipint → mosipid +``` + +--- + +## 3. Components you will touch + +| Piece | Path / link | Role | +|---|---|---| +| Image list | `release/vidivi/images.txt` | **Only** file changed in a normal transfer PR | +| Transfer tool | `release/vidivi/vidivi.py` | Local `check` / `hash` / `push` (optional; Actions uses this via reusable workflow) | +| Manual workflow | [Actions → Manual workflow to transfer images](https://github.com/mosip/release-script/actions/workflows/image-transfer.yml) | How transfers are executed in practice | +| Reusable logic | `mosip/kattu` → `image-transfer.yml` | Actual Crane copy + protections (e.g. `mosipid` admin-only) | +| Report | `release/vidivi/transfer_report.md` | Summary after an Actions run | +| Tool docs | [`release/vidivi/README.md`](../vidivi/README.md) | Inputs, secrets, troubleshooting | +| Release context | [`release/README.md`](../README.md) | Where transfer sits in a full release | + +--- + +## 4. End-to-end process (as followed today) + +### High-level flow + +``` +1. Need identified (sprint / bugfix / release) +2. DSD/Jira ticket created +3. Update release/vidivi/images.txt (source:tag → dest-tag) +4. Open PR on release-script (usually release-1.2.0.1 or active release branch) +5. PR reviewed & merged +6. Run GitHub Actions "Manual workflow to transfer images" +7. Select destination secret + destination org +8. Workflow: validate token → check images → Crane copy → Slack / report +9. Verify images on destination org +10. Close ticket (for prod: also Security signing ticket) +``` + +### Step-by-step (operator checklist) + +#### Step 1 — Confirm what must move + +Collect: + +- Source org + image name + **source tag** (must already exist) +- Destination org +- Destination tag (often same version line, sometimes shortened, e.g. `release-1.0.x` → `1.0.x`) +- Ticket id (e.g. `DSD-10681`) + +Confirm source image exists, for example: + +```bash +crane digest mosipdev/partner-management-service:release-1.2.2.x +# or +docker pull mosipdev/partner-management-service:release-1.2.2.x +``` + +#### Step 2 — Create / use the DSD ticket + +Title pattern used in practice: + +- `[DSD-xxxxx] Image transfer from dev to dev2` +- `[DSD-xxxxx] Image transfer from mosipdev to mosipqa` +- `[DSD-xxxxx] injistackdev2 to injistackqa` + +Ticket should list images/tags and reason (module release, QA handoff, patch, etc.). + +#### Step 3 — Edit `images.txt` + +File: `release/vidivi/images.txt` + +**Format (space-separated, one image per line):** + +``` +/: +``` + +Examples: + +``` +mosipdev/apitest-resident:develop develop +injistackdev2/uitest-web:release-1.0.x 1.0.x +mosipqa/kernel-auth-service:1.2.0.1 1.2.0.1 +``` + +Notes: + +- Destination **org is not** in this file — you choose it when running the workflow (`DESTINATION_ORGANIZATION`). +- The file is usually **replaced** for the next transfer batch (not an ever-growing append-only list). Recent PRs overwrite previous lines with the new set. +- Official library images (postgres, nginx) can be listed without `library/`. + +#### Step 4 — Open PR on `release-script` + +- Base branch: active release branch (commonly `release-1.2.0.1`) +- Title: match ticket, e.g. `[DSD-10761] Image transfer from dev to dev2 .` +- Change set: almost always **only** `release/vidivi/images.txt` +- Get PR reviewed and **merged** before running the workflow (workflow reads the file from the branch you select) + +#### Step 5 — Run the manual workflow + +1. Open [Manual workflow to transfer images](https://github.com/mosip/release-script/actions/workflows/image-transfer.yml) +2. Click **Run workflow** +3. Select the **branch** that contains the merged `images.txt` +4. Fill inputs (see cheat sheet below) +5. Run and watch the job + +#### Step 6 — Verify + +- Actions run succeeded +- Spot-check on Docker Hub / Harbor that destination tags exist +- Optional: `crane digest /:` matches source +- Attach run URL on the ticket; close ticket +- For **`mosipid`**: open Security image-signing ticket (release process) + +--- + +## 5. Workflow inputs cheat sheet (present process) + +| Input | Meaning | +|---|---| +| `USERNAME` | Docker Hub user or Harbor robot that can **push** to destination | +| `SECRET_NAME` | Which GitHub secret holds the destination token | +| `CUSTOM_SECRET_NAME` | Only if `SECRET_NAME=custom` | +| `DESTINATION_ORGANIZATION` | Dest org/project (`mosipdev2`, `mosipqa`, `mosipid`, …) | +| `REGISTRY_URL` | Default Docker Hub: `https://index.docker.io/v1/` | +| `REGISTRY_TYPE` | `dockerhub` / `harbor` / `other` | +| `ENABLE_WIREGUARD` | `true` for private Harbor behind VPN | + +### Typical combinations + +| Hop | `SECRET_NAME` | `DESTINATION_ORGANIZATION` | +|---|---|---| +| → `mosipdev2` | `MOSIPDEV2_DOCKER_TOKEN` | `mosipdev2` | +| → `mosipqa` | `MOSIPQA_DOCKER_TOKEN` | `mosipqa` | +| → `mosipint` | `MOSIPINT_DOCKER_TOKEN` | `mosipint` | +| → `mosipid` | `MOSIPID_DOCKER_TOKEN` | `mosipid` | +| → Inji orgs | `INJISTACK_DOCKER_TOKEN` or `custom` | e.g. `injistackqa` / `injistackdev2` | + +**Protected destination:** transfers to `mosipid` require **admin** on the calling repo (enforced in `mosip/kattu`). Non-admins should not use this for production. + +--- + +## 6. Scenario-based examples (from merged PRs) + +These are real merged PRs on `mosip/release-script`. Use them in KT to show “what good looks like.” + +### Scenario A — Dev → Dev2 (pre-QA staging) + +**When:** New builds are ready for pre-QA / stabilization in `mosipdev2` (or Inji `injistackdev2`). + +**Example PR:** [#1812](https://github.com/mosip/release-script/pull/1812) — `[DSD-10761] Image transfer from dev to dev2` +**Author:** abhishek8shankar · **Merged:** 2026-09-01 · **Base:** `release-1.2.0.1` + +`images.txt` content after PR: + +``` +mosipdev/apitest-resident:develop develop +``` + +**Meaning:** + +- Pull/copy from `mosipdev/apitest-resident:develop` +- Push as `mosipdev2/apitest-resident:develop` (dest org chosen in workflow) + +**Another Dev→Dev2 example:** [#1797](https://github.com/mosip/release-script/pull/1797) — `[DSD-10681]` + +``` +injistackdev/uitest-web:release-1.0.x release-1.0.x +injistackdev/apitest-mimoto:release-1.0.x release-1.0.x +``` + +**Workflow run (conceptual):** + +| Field | Value | +|---|---| +| Branch | `release-1.2.0.1` | +| `SECRET_NAME` | `MOSIPDEV2_DOCKER_TOKEN` (or Inji token if dest is injistackdev2) | +| `DESTINATION_ORGANIZATION` | `mosipdev2` or `injistackdev2` | + +**KT talking points:** + +- Ticket id in PR title +- Only `images.txt` changed +- Source org in file = where image already lives +- Destination org = workflow input, not the file + +--- + +### Scenario B — Same ticket, second hop Dev2 → QA + +**When:** Images validated on Dev2; QA needs them in `mosipqa` / `injistackqa`. + +**Paired PRs under the same ticket `DSD-10681`:** + +1. [#1797](https://github.com/mosip/release-script/pull/1797) — Dev → Dev2 (sources from `injistackdev`) +2. [#1798](https://github.com/mosip/release-script/pull/1798) — Dev2 → QA (sources from `injistackdev2`) + +`images.txt` in #1798: + +``` +injistackdev2/uitest-web:release-1.0.x 1.0.x +injistackdev2/apitest-mimoto:release-1.0.x 1.0.x +``` + +**Notice:** + +- Source org changed from `injistackdev` → `injistackdev2` (because images now live on Dev2) +- Destination tag shortened to `1.0.x` while source tag stayed `release-1.0.x` +- Separate PR + separate workflow run for the second hop (do not mix hops in one unclear batch) + +**Workflow:** `DESTINATION_ORGANIZATION=injistackqa` (or `mosipqa`), matching QA token. + +**Similar MOSIP pairs:** + +| Ticket | Dev → Dev2 | Dev2 → QA | +|---|---|---| +| DSD-10600 | [#1770](https://github.com/mosip/release-script/pull/1770) | [#1772](https://github.com/mosip/release-script/pull/1772) | +| DSD-10588 | [#1763](https://github.com/mosip/release-script/pull/1763) | [#1764](https://github.com/mosip/release-script/pull/1764) | + +**KT talking points:** + +- One hop per PR/run is the clean pattern +- After Dev→Dev2 succeeds, next PR sources from **dev2**, not still from **dev** +- Same DSD ticket can cover both hops, but each hop is its own PR + +--- + +### Scenario C — Direct-ish Dev → QA + +**When:** Team requests images straight into QA org (still via transfer; sources listed from `mosipdev`). + +**Example PR:** [#1789](https://github.com/mosip/release-script/pull/1789) — `[DSD-10643] image transfer from mosipdev to mosipqa` +**Author:** Ivanmeneges · **Merged:** 2026-08-07 + +``` +mosipdev/partner-management-service:release-1.2.2.x 1.2.2.x +mosipdev/policy-management-service:release-1.2.2.x 1.2.2.x +``` + +**Workflow:** `SECRET_NAME=MOSIPQA_DOCKER_TOKEN`, `DESTINATION_ORGANIZATION=mosipqa` + +**KT talking points:** + +- File still lists **source** as `mosipdev/...` +- Recommended lifecycle prefers Dev2 first; direct Dev→QA happens when teams request it / for specific modules — call out risk (less pre-QA soak) +- Similar: [#1766](https://github.com/mosip/release-script/pull/1766), [#1750](https://github.com/mosip/release-script/pull/1750) + +--- + +### Scenario D — QA → Interim (`mosipint` / Inji QA → int-style) + +**When:** QA-validated images needed for interim / implementation holding. + +**Example PR:** [#1768](https://github.com/mosip/release-script/pull/1768) — `[DSD-10599] Image transfer from qa to int` +**Author:** abhishek8shankar · **Merged:** 2026-07-30 + +``` +injistackqa/mimoto:1.0.x 1.0.x +injistackqa/inji-web:1.0.x 1.0.x +``` + +**Workflow:** destination org `mosipint` or the intended interim/Inji dest; use `MOSIPINT_DOCKER_TOKEN` when pushing to `mosipint`. + +**KT talking points:** + +- Source is now the **QA** org +- Interim is Release/DevOps-sensitive; treat carefully vs routine Dev2 hops + +--- + +### Scenario E — Toward production / community (`mosipid`) + infra images + +**When:** Release or shared infra images must land on `mosipid` / `mosipint`. + +**Example PR:** [#1813](https://github.com/mosip/release-script/pull/1813) — `[DSD-10771] moved the docker kafka and zookeeper docker image to mosipid and mosipint` +**Author:** Prafulrakhade · **Merged:** 2026-09-02 + +``` +praful02/kafka:3.5.1-debian-11-r0 3.5.1-debian-11-r0 +praful02/zookeeper:3.8.2-debian-11-r4 3.8.2-debian-11-r4 +``` + +**KT talking points:** + +- Source can be a non-`mosipdev` org when promoting third-party/mirrored images +- May require **two** workflow runs if pushing to both `mosipid` and `mosipint` (same `images.txt`, different `DESTINATION_ORGANIZATION` / secrets) +- `mosipid` is **admin-protected** in `kattu` +- After community/prod transfer: create **Security image signing** ticket (see `release/README.md` step 13) + +--- + +### Scenario F — Inji Dev2 → Inji QA (explicit org rename in file) + +**Example PR:** [#1809](https://github.com/mosip/release-script/pull/1809) — `[DSD-10756] injistackdev2 to injistackqa` + +Diff pattern: sources switched to `injistackdev2/...` before transfer into QA. + +Related nearby: + +- [#1808](https://github.com/mosip/release-script/pull/1808) — injistackdev → injistackdev2 +- [#1806](https://github.com/mosip/release-script/pull/1806) / [#1807](https://github.com/mosip/release-script/pull/1807) — mosipdev → mosipdev2 → mosipqa chain + +**KT talking points:** Inji follows the same PR + workflow ritual; only org names and sometimes tokens differ. + +--- + +## 7. How to walk a KT session (suggested agenda) + +| Time box | Topic | Demo artifact | +|---|---|---| +| 5 min | Why transfer exists / org map | Section 2 diagram | +| 10 min | File format + PR ritual | Open [#1812](https://github.com/mosip/release-script/pull/1812) diff | +| 10 min | Two-hop story | [#1797](https://github.com/mosip/release-script/pull/1797) → [#1798](https://github.com/mosip/release-script/pull/1798) | +| 10 min | Workflow UI + secret/org pairing | Live Actions form (dry explanation if no run) | +| 5 min | Prod / signing / admin protection | [#1813](https://github.com/mosip/release-script/pull/1813) + `release/README.md` | +| 5 min | Failures & checklist | Section 9 | +| 5 min | Q&A / who does what today | Section 8 | + +--- + +## 8. Who does what today (as-is) + +| Activity | Typical owner today | +|---|---| +| Build images into `mosipdev` / `injistackdev` | Module CI / developers | +| Raise DSD ticket + PR updating `images.txt` | Requester (Dev / QA / DevOps) — many PRs from DevOps engineers | +| Merge transfer PR | Reviewers with write access on release branch | +| Run Actions transfer workflow | Usually DevOps (holds org tokens / knows secret mapping) | +| Verify destination tags | Requester + DevOps | +| Transfer to `mosipid` / signing ticket | Release / DevOps + Security | + +> Note: Handover of Dev→Dev2 / Dev2→QA to Dev/QA teams is a **future** control change (Environments, stage workflows). This KT is the **current** operating procedure. + +--- + +## 9. Common mistakes & troubleshooting + +| Mistake / symptom | Why it hurts | What to do | +|---|---|---| +| Wrong source org in `images.txt` | Workflow looks for image that is not there | After Dev2 hop, source must be `*dev2`, not still `*dev` | +| Dest org in file instead of workflow | File format has no dest org | Put dest only in `DESTINATION_ORGANIZATION` | +| Ran workflow before PR merge | Branch still has old list | Merge first; select correct branch | +| Secret / org mismatch | Auth fail or push to wrong place | Match cheat sheet (Section 5) | +| Tag does not exist on source | `check` fails | Confirm digest/pull before PR | +| Overwrote `images.txt` for unrelated batch still needed | Previous list lost from tip of branch | OK for process (file is per-run manifest); keep ticket/PR history as audit | +| Non-admin → `mosipid` | Blocked by `kattu` | Use Release admin or request admin-run | +| Harbor timeout | Private network | `ENABLE_WIREGUARD=true` + valid `WIREGUARD_CONFIG` | +| CRLF / spacing in `images.txt` | Parse issues | One space between image and dest tag; Unix line endings | + +**Local dry checks (optional):** + +```bash +cd release/vidivi +# configure config.yml for the destination you intend +python3 vidivi.py check +python3 vidivi.py hash # optional digest compare +# prefer Actions push in shared process; local push only if you have tokens & policy allows +``` + +--- + +## 10. Quick reference card (print / slide) + +1. Ticket (`DSD-…`) +2. Edit `release/vidivi/images.txt` → `source-org/image:src-tag dest-tag` +3. PR → merge on release branch +4. Actions → Manual image transfer → branch + secret + **destination org** +5. Verify tags / digests → close ticket +6. If `mosipid`: Security signing ticket + +**Remember:** `images.txt` = **what** to copy from where; workflow inputs = **where** to push and **which credential**. + +--- + +## 11. Reference PR index (recent patterns) + +| PR | Hop (from title) | Useful for | +|---|---|---| +| [#1812](https://github.com/mosip/release-script/pull/1812) | Dev → Dev2 | Single-image Dev2 handoff | +| [#1797](https://github.com/mosip/release-script/pull/1797) / [#1798](https://github.com/mosip/release-script/pull/1798) | Dev→Dev2 then Dev2→QA | Same ticket, two hops | +| [#1789](https://github.com/mosip/release-script/pull/1789) | Dev → QA | Direct-to-QA request | +| [#1768](https://github.com/mosip/release-script/pull/1768) | QA → Int | Interim promotion | +| [#1813](https://github.com/mosip/release-script/pull/1813) | → mosipid / mosipint | Prod-bound / infra images | +| [#1809](https://github.com/mosip/release-script/pull/1809) | injistackdev2 → injistackqa | Inji QA path | +| [#1806](https://github.com/mosip/release-script/pull/1806) / [#1807](https://github.com/mosip/release-script/pull/1807) | mosipdev → dev2 → qa | Full lower-stage chain | + +Search more: GitHub PRs filter `is:merged image transfer` in `mosip/release-script`. + +--- + +## 12. Related docs + +| Doc | Use | +|---|---| +| [Vidivi README](../vidivi/README.md) | Tool details, secrets, Harbor, Crane | +| [Release README](../README.md) | Where transfer sits in full release | +| [Image transfer handover plan](./image-transfer-handover-plan.md) | Future Dev/QA ownership model | +| [GitHub Environments how-to](./github-environments-image-transfer.md) | Future approval gates | +| [WG approval test](./wg-lifecycle-approval-test.md) | Practising Environment Approve/Reject | + +--- + +**Document purpose:** Enable anyone to explain and execute the **current** MOSIP/Inji image transfer process using real release-script PRs as teaching examples. From 307111e924c377094684df17e603ca93cb80c00e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 3 Sep 2026 03:47:29 +0000 Subject: [PATCH 7/9] docs: link image transfer KT from Vidivi README Co-authored-by: Ivanmeneges --- release/vidivi/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/release/vidivi/README.md b/release/vidivi/README.md index 92e776ec..915584c5 100644 --- a/release/vidivi/README.md +++ b/release/vidivi/README.md @@ -4,6 +4,8 @@ ## Overview +For a step-by-step **Knowledge Transfer** on the current process (with scenario examples from merged PRs), see **[Image Transfer KT](../docs/image-transfer-kt.md)**. + For handing stage transfers (`mosipdev` → `mosipdev2`, `mosipdev2` → `mosipqa`) from DevOps to Dev/QA with access control, approvals, and monitoring, see **[Image Transfer Handover Plan](../docs/image-transfer-handover-plan.md)** and **[GitHub Environments setup (full steps)](../docs/github-environments-image-transfer.md)**. In MOSIP, we maintain several Docker Hub organizations with specific purposes: From 0d7cbbc40b1d8c95f141a88b35312b77472dffde Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 7 Sep 2026 07:28:51 +0000 Subject: [PATCH 8/9] feat: single approval-based image-transfer workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace free-form SECRET_NAME/DESTINATION_ORGANIZATION with TRANSFER_TARGET bound to GitHub Environments. One YAML maps target → destination org and uses Environment secret DOCKER_TOKEN after required-reviewer approval. Remove per-hop workflow files; document the single-workflow approach. Co-authored-by: Ivanmeneges --- .github/workflows/image-transfer-dev2.yml | 49 ------ .github/workflows/image-transfer-qa.yml | 49 ------ .github/workflows/image-transfer.yml | 143 ++++++++---------- .../github-environments-image-transfer.md | 109 +++---------- ...image-transfer-approval-single-workflow.md | 137 +++++++++++++++++ release/docs/image-transfer-handover-plan.md | 36 ++--- release/vidivi/README.md | 98 +++++------- 7 files changed, 281 insertions(+), 340 deletions(-) delete mode 100644 .github/workflows/image-transfer-dev2.yml delete mode 100644 .github/workflows/image-transfer-qa.yml create mode 100644 release/docs/image-transfer-approval-single-workflow.md diff --git a/.github/workflows/image-transfer-dev2.yml b/.github/workflows/image-transfer-dev2.yml deleted file mode 100644 index 906f86e6..00000000 --- a/.github/workflows/image-transfer-dev2.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Transfer images to mosipdev2 - -# Stage-scoped transfer: destination is fixed to mosipdev2. -# Requires GitHub Environment "transfer-dev2" with required reviewers -# and Environment secret MOSIPDEV2_DOCKER_TOKEN. -# See: release/docs/github-environments-image-transfer.md - -on: - workflow_dispatch: - inputs: - USERNAME: - description: 'Registry username (Docker Hub user or Harbor robot)' - required: true - default: '' - type: string - REGISTRY_URL: - description: 'Destination registry URL' - required: true - default: 'https://index.docker.io/v1/' - type: string - REGISTRY_TYPE: - description: 'Destination registry type' - required: true - default: 'dockerhub' - type: choice - options: - - dockerhub - - harbor - - other - ENABLE_WIREGUARD: - description: 'Enable WireGuard VPN (required for private Harbor networks)' - required: false - default: false - type: boolean - -jobs: - Image-transfer: - environment: transfer-dev2 - uses: mosip/kattu/.github/workflows/image-transfer.yml@master - with: - DESTINATION_ORGANIZATION: mosipdev2 - REGISTRY_URL: ${{ inputs.REGISTRY_URL }} - REGISTRY_TYPE: ${{ inputs.REGISTRY_TYPE }} - ENABLE_WIREGUARD: ${{ inputs.ENABLE_WIREGUARD }} - USERNAME: ${{ inputs.USERNAME }} - secrets: - TOKEN: ${{ secrets.MOSIPDEV2_DOCKER_TOKEN }} - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_DEVOPS }} - WIREGUARD_CONFIG: ${{ secrets.WIREGUARD_CONFIG }} diff --git a/.github/workflows/image-transfer-qa.yml b/.github/workflows/image-transfer-qa.yml deleted file mode 100644 index 5e64dd8b..00000000 --- a/.github/workflows/image-transfer-qa.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Transfer images to mosipqa - -# Stage-scoped transfer: destination is fixed to mosipqa. -# Requires GitHub Environment "transfer-qa" with required reviewers -# and Environment secret MOSIPQA_DOCKER_TOKEN. -# See: release/docs/github-environments-image-transfer.md - -on: - workflow_dispatch: - inputs: - USERNAME: - description: 'Registry username (Docker Hub user or Harbor robot)' - required: true - default: '' - type: string - REGISTRY_URL: - description: 'Destination registry URL' - required: true - default: 'https://index.docker.io/v1/' - type: string - REGISTRY_TYPE: - description: 'Destination registry type' - required: true - default: 'dockerhub' - type: choice - options: - - dockerhub - - harbor - - other - ENABLE_WIREGUARD: - description: 'Enable WireGuard VPN (required for private Harbor networks)' - required: false - default: false - type: boolean - -jobs: - Image-transfer: - environment: transfer-qa - uses: mosip/kattu/.github/workflows/image-transfer.yml@master - with: - DESTINATION_ORGANIZATION: mosipqa - REGISTRY_URL: ${{ inputs.REGISTRY_URL }} - REGISTRY_TYPE: ${{ inputs.REGISTRY_TYPE }} - ENABLE_WIREGUARD: ${{ inputs.ENABLE_WIREGUARD }} - USERNAME: ${{ inputs.USERNAME }} - secrets: - TOKEN: ${{ secrets.MOSIPQA_DOCKER_TOKEN }} - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_DEVOPS }} - WIREGUARD_CONFIG: ${{ secrets.WIREGUARD_CONFIG }} diff --git a/.github/workflows/image-transfer.yml b/.github/workflows/image-transfer.yml index beca8787..afcfaf54 100644 --- a/.github/workflows/image-transfer.yml +++ b/.github/workflows/image-transfer.yml @@ -1,42 +1,40 @@ name: Manual workflow to transfer images +# Single approval-based transfer workflow. +# Operator picks TRANSFER_TARGET → job waits on that GitHub Environment → +# after Approve, destination org is derived and DOCKER_TOKEN (Environment secret) is used. +# +# Setup: release/docs/github-environments-image-transfer.md +# release/docs/image-transfer-approval-single-workflow.md + on: workflow_dispatch: inputs: - USERNAME: - description: 'provide docker hub username' + TRANSFER_TARGET: + description: 'Stage gate (GitHub Environment). Destination org is derived from this — do not pick freely.' required: true - default: '' - type: string - SECRET_NAME: - description: 'Select the GitHub secret name for Docker registry token' - required: true - default: 'MOSIPDEV2_DOCKER_TOKEN' type: choice + default: 'transfer-dev2' options: - - MOSIPDEV2_DOCKER_TOKEN - - MOSIPQA_DOCKER_TOKEN - - MOSIPID_DOCKER_TOKEN - - MOSIPINT_DOCKER_TOKEN - - INJISTACK_DOCKER_TOKEN - - custom - CUSTOM_SECRET_NAME: - description: 'If SECRET_NAME is "custom", enter the GitHub secret name here' - required: false - default: '' - type: string - DESTINATION_ORGANIZATION: - description: 'provide docker hub destination org' + - transfer-dev2 + - transfer-qa + - transfer-mosipint + - transfer-mosipid + - transfer-injistack-dev2 + - transfer-injistack-qa + - transfer-injistack + USERNAME: + description: 'Registry username (Docker Hub user or Harbor robot)' required: true default: '' type: string REGISTRY_URL: - description: 'provide destination registry URL (http://harbor.example.com or https://registry.example.com)' + description: 'Destination registry URL' required: true default: 'https://index.docker.io/v1/' type: string REGISTRY_TYPE: - description: 'Select destination registry type' + description: 'Destination registry type' required: true default: 'dockerhub' type: choice @@ -51,75 +49,62 @@ on: type: boolean jobs: - chk_token: + # Maps the selected gate → fixed destination org (no free-typed org). + resolve-target: runs-on: ubuntu-latest outputs: - TOKEN: ${{ steps.ORG_TOKEN.outputs.TOKEN }} + DESTINATION_ORGANIZATION: ${{ steps.map.outputs.DESTINATION_ORGANIZATION }} + TRANSFER_TARGET: ${{ inputs.TRANSFER_TARGET }} steps: - - name: Resolve secret name - id: ORG_TOKEN + - name: Map TRANSFER_TARGET to destination organization + id: map env: - SELECTED_SECRET: ${{ inputs.SECRET_NAME }} - CUSTOM_SECRET: ${{ inputs.CUSTOM_SECRET_NAME }} + TARGET: ${{ inputs.TRANSFER_TARGET }} run: | - if [ "$SELECTED_SECRET" = "custom" ]; then - # User selected custom — CUSTOM_SECRET_NAME is required - if [ -z "$CUSTOM_SECRET" ]; then - printf '❌ ERROR: CUSTOM_SECRET_NAME is required when SECRET_NAME is set to "custom"\n' >&2 - printf 'Please provide the GitHub secret name in the CUSTOM_SECRET_NAME field\n' >&2 - exit 1 - fi - - # Validate custom secret name format (GitHub only allows [A-Z0-9_], must start with [A-Z_]) - if ! printf '%s' "$CUSTOM_SECRET" | grep -qE '^[A-Za-z_][A-Za-z0-9_]*$'; then - printf '❌ ERROR: Invalid secret name: "%s"\n' "$CUSTOM_SECRET" >&2 - printf 'GitHub secret names must:\n' >&2 - printf ' - Start with a letter or underscore\n' >&2 - printf ' - Contain only letters, numbers, and underscores\n' >&2 - printf ' - No spaces, hyphens, or special characters\n' >&2 - printf 'Example: MY_ORG_DOCKER_TOKEN\n' >&2 + case "$TARGET" in + transfer-dev2) + DEST="mosipdev2" + ;; + transfer-qa) + DEST="mosipqa" + ;; + transfer-mosipint) + DEST="mosipint" + ;; + transfer-mosipid) + DEST="mosipid" + ;; + transfer-injistack-dev2) + DEST="injistackdev2" + ;; + transfer-injistack-qa) + DEST="injistackqa" + ;; + transfer-injistack) + DEST="injistack" + ;; + *) + printf '❌ Unknown TRANSFER_TARGET: %s\n' "$TARGET" >&2 exit 1 - fi - - TOKEN_SECRET="$CUSTOM_SECRET" - printf 'Using custom secret name: %s\n' "$TOKEN_SECRET" - else - TOKEN_SECRET="$SELECTED_SECRET" - printf 'Using predefined secret name: %s\n' "$TOKEN_SECRET" - fi - - printf 'TOKEN=%s\n' "$TOKEN_SECRET" >> "$GITHUB_OUTPUT" - - - name: Validate secret configuration - env: - TOKEN_EXISTS: ${{ secrets[steps.ORG_TOKEN.outputs.TOKEN] != '' }} - SECRET_NAME: ${{ steps.ORG_TOKEN.outputs.TOKEN }} - DESTINATION_ORGANIZATION: ${{ inputs.DESTINATION_ORGANIZATION }} - run: | - if [ "$TOKEN_EXISTS" != "true" ]; then - printf '❌ ERROR: Secret '\''%s'\'' is not configured or is empty\n' "$SECRET_NAME" - printf '\n' - printf 'Please configure the following secret in GitHub repository settings:\n' - printf ' Secret name: %s\n' "$SECRET_NAME" - printf ' Path: Settings → Secrets and variables → Actions → New repository secret\n' - printf '\n' - printf 'For organization '\''%s'\'', you need:\n' "$DESTINATION_ORGANIZATION" - printf ' - Secret: %s\n' "$SECRET_NAME" - printf ' - Value: Your Docker registry token/password\n' - exit 1 - fi - - printf '✅ Secret '\''%s'\'' is configured\n' "$SECRET_NAME" + ;; + esac + printf 'TRANSFER_TARGET=%s\n' "$TARGET" + printf 'DESTINATION_ORGANIZATION=%s\n' "$DEST" + printf 'DESTINATION_ORGANIZATION=%s\n' "$DEST" >> "$GITHUB_OUTPUT" + + # Waits for Environment required reviewers, then unlocks Environment secret DOCKER_TOKEN. Image-transfer: - needs: chk_token + needs: resolve-target + environment: ${{ inputs.TRANSFER_TARGET }} uses: mosip/kattu/.github/workflows/image-transfer.yml@master with: - DESTINATION_ORGANIZATION: ${{ inputs.DESTINATION_ORGANIZATION }} + DESTINATION_ORGANIZATION: ${{ needs.resolve-target.outputs.DESTINATION_ORGANIZATION }} REGISTRY_URL: ${{ inputs.REGISTRY_URL }} REGISTRY_TYPE: ${{ inputs.REGISTRY_TYPE }} ENABLE_WIREGUARD: ${{ inputs.ENABLE_WIREGUARD }} USERNAME: ${{ inputs.USERNAME }} secrets: - TOKEN: "${{ secrets[needs.chk_token.outputs.TOKEN] }}" + # Same secret NAME on every Environment; VALUE differs per Environment. + TOKEN: ${{ secrets.DOCKER_TOKEN }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_DEVOPS }} WIREGUARD_CONFIG: ${{ secrets.WIREGUARD_CONFIG }} diff --git a/release/docs/github-environments-image-transfer.md b/release/docs/github-environments-image-transfer.md index 5886d534..cb78accd 100644 --- a/release/docs/github-environments-image-transfer.md +++ b/release/docs/github-environments-image-transfer.md @@ -27,7 +27,7 @@ Today, anyone who can run the manual image-transfer workflow can aim it at any o | Click **Run workflow** → job starts immediately | Click **Run workflow** → job waits for Approver | | Repo secrets readable by any write-access workflow | Docker push token locked in Environment; released only after approval | | Weak audit of “who allowed this” | Approver name stored on the deployment | -| One workflow can target any destination | Stage workflow + fixed Environment = fixed destination | +| One workflow can target any destination freely | One workflow + `TRANSFER_TARGET` Environment = fixed destination per gate | ``` Operator clicks "Run workflow" @@ -63,13 +63,21 @@ Deployment history + Actions log = audit trail ## Target setup for MOSIP image transfer -| Environment name | Used by workflow | Required reviewers | Environment secret(s) | Fixed destination org | -|---|---|---|---|---| -| `transfer-dev2` | `image-transfer-dev2.yml` | Dev Approver team | `MOSIPDEV2_DOCKER_TOKEN` | `mosipdev2` | -| `transfer-qa` | `image-transfer-qa.yml` | QA Approver team | `MOSIPQA_DOCKER_TOKEN` | `mosipqa` | -| `transfer-prod` | `image-transfer-prod.yml` | Release admins | `MOSIPID_DOCKER_TOKEN`, `MOSIPINT_DOCKER_TOKEN` | `mosipid` / `mosipint` | +**Preferred model: one workflow** (`.github/workflows/image-transfer.yml`) + several Environments. +See [Approval-based single workflow](./image-transfer-approval-single-workflow.md). -Repo-level secrets that can stay shared (not stage-specific): `SLACK_WEBHOOK_DEVOPS`, `WIREGUARD_CONFIG`. +| Environment name (`TRANSFER_TARGET`) | Required reviewers | Environment secret | Fixed destination org | +|---|---|---|---| +| `transfer-dev2` | Dev Approver team | `DOCKER_TOKEN` | `mosipdev2` | +| `transfer-qa` | QA Approver team | `DOCKER_TOKEN` | `mosipqa` | +| `transfer-mosipint` | Release / DevOps | `DOCKER_TOKEN` | `mosipint` | +| `transfer-mosipid` | Release admins | `DOCKER_TOKEN` | `mosipid` | +| `transfer-injistack-dev2` | Inji/Dev approvers | `DOCKER_TOKEN` | `injistackdev2` | +| `transfer-injistack-qa` | Inji/QA approvers | `DOCKER_TOKEN` | `injistackqa` | +| `transfer-injistack` | Release | `DOCKER_TOKEN` | `injistack` | + +Use the **same secret name** `DOCKER_TOKEN` on every Environment; only the token **value** changes. +Repo-level secrets that can stay shared: `SLACK_WEBHOOK_DEVOPS`, `WIREGUARD_CONFIG`. --- @@ -159,89 +167,18 @@ Until you remove repo copies, a workflow **without** an Environment can still us --- -### Phase C — Add stage workflows (code) - -Add separate workflows so destination cannot be mistyped. +### Phase C — Use the single approval workflow (code) -#### Example: Dev2 workflow +Keep **one** file: `.github/workflows/image-transfer.yml` with: -File: `.github/workflows/image-transfer-dev2.yml` - -```yaml -name: Transfer images to mosipdev2 - -on: - workflow_dispatch: - inputs: - USERNAME: - description: 'Registry username (Docker Hub user or Harbor robot)' - required: true - type: string - REGISTRY_URL: - description: 'Destination registry URL' - required: true - default: 'https://index.docker.io/v1/' - type: string - REGISTRY_TYPE: - description: 'Destination registry type' - required: true - default: 'dockerhub' - type: choice - options: - - dockerhub - - harbor - - other - ENABLE_WIREGUARD: - description: 'Enable WireGuard VPN for private Harbor' - required: false - default: false - type: boolean - -jobs: - # Gate: waits for Environment approval; unlocks Environment secrets - Image-transfer: - environment: transfer-dev2 - uses: mosip/kattu/.github/workflows/image-transfer.yml@master - with: - DESTINATION_ORGANIZATION: mosipdev2 # FIXED — operators cannot change this - REGISTRY_URL: ${{ inputs.REGISTRY_URL }} - REGISTRY_TYPE: ${{ inputs.REGISTRY_TYPE }} - ENABLE_WIREGUARD: ${{ inputs.ENABLE_WIREGUARD }} - USERNAME: ${{ inputs.USERNAME }} - secrets: - # From Environment secret (available only after approval) - TOKEN: ${{ secrets.MOSIPDEV2_DOCKER_TOKEN }} - # From repository secrets - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_DEVOPS }} - WIREGUARD_CONFIG: ${{ secrets.WIREGUARD_CONFIG }} -``` +- Input `TRANSFER_TARGET` (choice = Environment name) +- Job `environment: ${{ inputs.TRANSFER_TARGET }}` +- Destination org **mapped** from target (not free text) +- `TOKEN: ${{ secrets.DOCKER_TOKEN }}` (Environment secret) -#### Example: QA workflow - -File: `.github/workflows/image-transfer-qa.yml` - -Same shape, but: - -- `environment: transfer-qa` -- `DESTINATION_ORGANIZATION: mosipqa` -- `TOKEN: ${{ secrets.MOSIPQA_DOCKER_TOKEN }}` - -#### Example: Prod workflow (DevOps only) - -- `environment: transfer-prod` -- Input choice limited to `mosipid` / `mosipint` only -- Tokens from prod Environment secrets -- Optionally restrict who can see/run it via repo permissions + admin-only kattu protection - -#### What to do with the old generic workflow - -| Option | Recommendation | -|---|---| -| A. Restrict to admins / leave as break-glass | Short term OK | -| B. Delete after stage workflows are proven | Preferred long term | -| C. Point it at `transfer-prod` only | If you still need one flexible entry for Release | +Full detail: [image-transfer-approval-single-workflow.md](./image-transfer-approval-single-workflow.md). -Do **not** leave the old workflow able to read Dev2/QA tokens from repository secrets. +Multiple per-hop YAML files are **not required**. --- diff --git a/release/docs/image-transfer-approval-single-workflow.md b/release/docs/image-transfer-approval-single-workflow.md new file mode 100644 index 00000000..4a130286 --- /dev/null +++ b/release/docs/image-transfer-approval-single-workflow.md @@ -0,0 +1,137 @@ +# Approval-based image transfer — single workflow (not multiple YAMLs) + +## Short answer + +You **do not need** one workflow file per hop (`*-dev2.yml`, `*-qa.yml`, …). + +Use **one** workflow (`image-transfer.yml`) and select a **TRANSFER_TARGET**. That value is the GitHub Environment name. GitHub then: + +1. Pauses the job for that Environment’s **required reviewers** +2. After Approve, unlocks that Environment’s **DOCKER_TOKEN** +3. Pushes only to the **destination org mapped from that target** + +Multiple YAMLs were only one optional design. **One YAML + multiple Environments** is enough and matches how you work today (one Actions entry point). + +--- + +## What changes are needed + +### A. GitHub UI (must do — cannot be done by YAML alone) + +Create these Environments under `mosip/release-script` → **Settings** → **Environments**: + +| Environment name (`TRANSFER_TARGET`) | Required reviewers | Environment secret | Pushes to | +|---|---|---|---| +| `transfer-dev2` | Dev approvers | `DOCKER_TOKEN` = mosipdev2 token | `mosipdev2` | +| `transfer-qa` | QA approvers | `DOCKER_TOKEN` = mosipqa token | `mosipqa` | +| `transfer-mosipint` | Release / DevOps | `DOCKER_TOKEN` = mosipint token | `mosipint` | +| `transfer-mosipid` | Release admins | `DOCKER_TOKEN` = mosipid token | `mosipid` | +| `transfer-injistack-dev2` | Inji/Dev approvers | `DOCKER_TOKEN` = injistackdev2 token | `injistackdev2` | +| `transfer-injistack-qa` | Inji/QA approvers | `DOCKER_TOKEN` = injistackqa token | `injistackqa` | +| `transfer-injistack` | Release | `DOCKER_TOKEN` = injistack token | `injistack` | + +On each Environment: + +1. **Required reviewers** (users/teams; max 6; any one approval is enough) +2. **Prevent self-review** +3. Optional: wait timer, allowed branches (`release-1.2.0.1` / `master`) +4. Secret name must be exactly **`DOCKER_TOKEN`** on every Environment (values differ) + +Then **remove or stop using** the old repo-level org tokens for routine runs (`MOSIPDEV2_DOCKER_TOKEN`, etc.) so nothing can bypass the gate. Keep `SLACK_WEBHOOK_DEVOPS` and `WIREGUARD_CONFIG` as repository secrets. + +### B. Workflow YAML (already the single-file model) + +File: `.github/workflows/image-transfer.yml` + +| Before (today’s misuse risk) | After (approval-based, one file) | +|---|---| +| Free `DESTINATION_ORGANIZATION` text | Derived from `TRANSFER_TARGET` | +| Free `SECRET_NAME` choice | Fixed Environment secret `DOCKER_TOKEN` | +| Job starts immediately | `environment: ${{ inputs.TRANSFER_TARGET }}` → waits for Approve | +| One workflow, weak binding | One workflow, strong binding via Environments | + +Flow inside the YAML: + +``` +resolve-target → map TRANSFER_TARGET → DESTINATION_ORGANIZATION + ↓ +Image-transfer → environment: TRANSFER_TARGET (approval gate) + → secrets.DOCKER_TOKEN (unlocked after Approve) + → calls mosip/kattu image-transfer reusable workflow +``` + +### C. Process (unchanged ritual, plus Approve) + +1. Ticket + PR updating `release/vidivi/images.txt` (same as KT) +2. Merge PR +3. **Run workflow** → pick `TRANSFER_TARGET` (e.g. `transfer-qa`) + username/registry +4. Job **Waiting** → Approver **Review deployments** → Approve / Reject +5. Transfer runs → verify tags → close ticket + +### D. No change required in `kattu` for basic gating + +`environment:` on the **caller** job is enough for Approve + Environment secrets. Existing `mosipid` admin protection in `kattu` remains an extra layer. + +Optional later in `kattu`: allowlist source org prefixes per destination. + +--- + +## Why one YAML is enough + +| Concern | How one YAML handles it | +|---|---| +| Different approvers per hop | Different Environment configs (reviewers), same workflow file | +| Different tokens per hop | Same secret **name** `DOCKER_TOKEN`, different **value** per Environment | +| Operator picks wrong org | Org is **mapped** from `TRANSFER_TARGET`; not free text | +| Audit | Each Environment has its own deployment history | +| Same UX as today | Still one Actions workflow to click | + +You only need multiple YAML files if you want **separate Actions menu entries** (harder to mis-click target). That is optional UX, not a security requirement. + +--- + +## Mapping reference (encoded in the workflow) + +| `TRANSFER_TARGET` | Destination org | +|---|---| +| `transfer-dev2` | `mosipdev2` | +| `transfer-qa` | `mosipqa` | +| `transfer-mosipint` | `mosipint` | +| `transfer-mosipid` | `mosipid` | +| `transfer-injistack-dev2` | `injistackdev2` | +| `transfer-injistack-qa` | `injistackqa` | +| `transfer-injistack` | `injistack` | + +To add a new hop later: add a choice option + one `case` arm + create the matching GitHub Environment with `DOCKER_TOKEN`. + +--- + +## Operator vs Approver access (reminder) + +| Role | Repo access | Also need | +|---|---|---| +| Operator (PR + Run workflow) | **Write** | — | +| Approver (Approve deployment) | **Read** minimum | Listed on that Environment’s required reviewers | +| Admin (create Environments / secrets) | **Admin** | — | + +--- + +## Rollout checklist + +- [ ] Create Environments listed above with reviewers + prevent self-review +- [ ] Add `DOCKER_TOKEN` on each Environment (correct org push token) +- [ ] Merge updated single `image-transfer.yml` +- [ ] Test `transfer-dev2` with a small `images.txt` batch (Approve path) +- [ ] Test Reject path once +- [ ] Test prevent self-review (operator cannot approve own run) +- [ ] Remove old repo-level Docker org tokens used by the previous free-form inputs +- [ ] Train operators: pick **TRANSFER_TARGET**, not a free destination string + +--- + +## Related docs + +- [Image Transfer KT (as-is process)](./image-transfer-kt.md) +- [Handover plan](./image-transfer-handover-plan.md) +- [GitHub Environments deep dive](./github-environments-image-transfer.md) +- [Vidivi README](../vidivi/README.md) diff --git a/release/docs/image-transfer-handover-plan.md b/release/docs/image-transfer-handover-plan.md index 739cbc6e..96add99d 100644 --- a/release/docs/image-transfer-handover-plan.md +++ b/release/docs/image-transfer-handover-plan.md @@ -126,23 +126,24 @@ Implement all layers. No single control is enough. | CODEOWNERS | `/release/vidivi/images-dev2.txt` → Dev leads; `/release/vidivi/images-qa.txt` → QA leads | | Admin list | Keep `mosipid` admin-only protection in `mosip/kattu` | -### 3. Split workflows by stage (recommended) +### 3. One approval-based workflow (recommended — not multiple YAMLs) -Replace “one mega-workflow anyone can aim at any org” with stage-scoped workflows: +Keep a **single** `image-transfer.yml`. Operator selects `TRANSFER_TARGET` (GitHub Environment name). The workflow: -| Workflow | Destination fixed to | Secret | Environment | -|---|---|---|---| -| `image-transfer-dev2.yml` | `mosipdev2` only | `MOSIPDEV2_DOCKER_TOKEN` | `transfer-dev2` | -| `image-transfer-qa.yml` | `mosipqa` only | `MOSIPQA_DOCKER_TOKEN` | `transfer-qa` | -| `image-transfer-prod.yml` | `mosipid` / `mosipint` | `MOSIPID_*` / `MOSIPINT_*` | `transfer-prod` (admins + required reviewers) | +- Sets `environment: ${{ inputs.TRANSFER_TARGET }}` (Approve / Reject gate) +- **Derives** destination org from that target (no free-typed org) +- Uses Environment secret `DOCKER_TOKEN` (same name, different value per Environment) -Benefits: +| `TRANSFER_TARGET` | Destination | Who approves | +|---|---|---| +| `transfer-dev2` | `mosipdev2` | Dev approvers | +| `transfer-qa` | `mosipqa` | QA approvers | +| `transfer-mosipint` / `transfer-mosipid` | `mosipint` / `mosipid` | Release / admins | +| Inji targets | `injistack*` | Inji / Release as configured | -- Operators cannot “accidentally” select `MOSIPID_DOCKER_TOKEN`. -- Environment **required reviewers** gate the run. -- Audit logs clearly show which stage workflow ran. +Full change list: [Approval-based single workflow](./image-transfer-approval-single-workflow.md). -Keep the existing generic workflow for DevOps break-glass only, or remove it after migration. +Multiple YAML files per hop are **optional UX only**, not required for security. ### 4. GitHub Environments (approval gates) @@ -273,12 +274,11 @@ Unchanged ownership: Release/DevOps only, admin-protected org, Security signing ### Phase 2 — Technical gates (recommended core) -- Split workflows: `dev2`, `qa`, `prod`. -- Create GitHub Environments with required reviewers + environment-scoped secrets. -- Move tokens off shared repo-secret usage where possible. -- Restrict Environment access to Operator GitHub teams. -- Tighten `kattu` allowlists (source org + destination org). -- Keep generic workflow admin-only or retire it. +- Update single `image-transfer.yml` with `TRANSFER_TARGET` + Environment approval (see [single-workflow guide](./image-transfer-approval-single-workflow.md)). +- Create GitHub Environments with required reviewers + Environment secret `DOCKER_TOKEN` each. +- Restrict who can approve per Environment (Dev vs QA vs Release). +- Remove old free-form repo Docker tokens after Environments work. +- Keep `kattu` allowlists / admin protection for `mosipid`. ### Phase 3 — Observability & hygiene diff --git a/release/vidivi/README.md b/release/vidivi/README.md index 915584c5..0edea88e 100644 --- a/release/vidivi/README.md +++ b/release/vidivi/README.md @@ -6,7 +6,7 @@ For a step-by-step **Knowledge Transfer** on the current process (with scenario examples from merged PRs), see **[Image Transfer KT](../docs/image-transfer-kt.md)**. -For handing stage transfers (`mosipdev` → `mosipdev2`, `mosipdev2` → `mosipqa`) from DevOps to Dev/QA with access control, approvals, and monitoring, see **[Image Transfer Handover Plan](../docs/image-transfer-handover-plan.md)** and **[GitHub Environments setup (full steps)](../docs/github-environments-image-transfer.md)**. +For approval-based transfer in **one** workflow (not multiple YAMLs), see **[Approval-based single workflow](../docs/image-transfer-approval-single-workflow.md)**. Related: **[Handover Plan](../docs/image-transfer-handover-plan.md)** · **[GitHub Environments](../docs/github-environments-image-transfer.md)**. In MOSIP, we maintain several Docker Hub organizations with specific purposes: @@ -189,98 +189,78 @@ Executing: crane copy mosipdev/kernel-auth-service:1.2.0.1 registry.example.com/ ## GitHub Actions Workflow +> **Approval-based (single YAML):** pick `TRANSFER_TARGET` → job waits on that GitHub Environment → after Approve, destination org is derived and Environment secret `DOCKER_TOKEN` is used. +> Setup guide: [Approval-based single workflow](../docs/image-transfer-approval-single-workflow.md). + ### Workflow Inputs Execute the [Manual workflow to transfer images](https://github.com/mosip/release-script/actions/workflows/image-transfer.yml) from the **release-script** repository. | Input | Description | Required | Default | Example | |-------|-------------|----------|---------|---------| +| `TRANSFER_TARGET` | Stage gate = GitHub Environment name (destination org is derived) | Yes | `transfer-dev2` | `transfer-qa`, `transfer-mosipid` | | `USERNAME` | Registry username | Yes | - | `robot$mosipdev+release-bot` (Harbor)
`myusername` (Docker Hub) | -| `SECRET_NAME` | Select the GitHub secret name for Docker registry token (dropdown) | Yes | `MOSIPDEV2_DOCKER_TOKEN` | `MOSIPID_DOCKER_TOKEN`, `custom` | -| `CUSTOM_SECRET_NAME` | Custom secret name (only required if `SECRET_NAME` is set to `custom`) | No | - | `MY_ORG_DOCKER_TOKEN` | -| `DESTINATION_ORGANIZATION` | Destination org/project | Yes | - | `mosipid`, `mosipqa`, `myproject` | | `REGISTRY_URL` | Destination registry URL | Yes | `https://index.docker.io/v1/` | `https://harbor.example.com` | | `REGISTRY_TYPE` | Registry type | Yes | `dockerhub` | `dockerhub`, `harbor`, `other` | | `ENABLE_WIREGUARD` | Enable VPN for private networks | No | `false` | `true` or `false` | -**`SECRET_NAME` Options:** +**`TRANSFER_TARGET` → destination org:** -| Option | Description | +| `TRANSFER_TARGET` | Destination organization | |---|---| -| `MOSIPDEV2_DOCKER_TOKEN` | Token for `mosipdev2` organization | -| `MOSIPQA_DOCKER_TOKEN` | Token for `mosipqa` organization | -| `MOSIPID_DOCKER_TOKEN` | Token for `mosipid` organization | -| `MOSIPINT_DOCKER_TOKEN` | Token for `mosipint` organization | -| `INJISTACK_DOCKER_TOKEN` | Token for `injistack` organization | -| `custom` | Enter your own secret name in `CUSTOM_SECRET_NAME` field | +| `transfer-dev2` | `mosipdev2` | +| `transfer-qa` | `mosipqa` | +| `transfer-mosipint` | `mosipint` | +| `transfer-mosipid` | `mosipid` | +| `transfer-injistack-dev2` | `injistackdev2` | +| `transfer-injistack-qa` | `injistackqa` | +| `transfer-injistack` | `injistack` | ### Workflow Secrets -**Required Secrets:** -1. **``**: Registry authentication token — select from predefined options or provide a custom name - - Docker Hub: Personal Access Token or Account Password - - Harbor: Robot account token - - Other registries: Appropriate authentication token - -2. **`SLACK_WEBHOOK_DEVOPS`**: Slack notification webhook (shared across all workflows) +**Environment secrets** (on each GitHub Environment named above): -3. **`WIREGUARD_CONFIG`**: (Optional) WireGuard VPN configuration for private registries +1. **`DOCKER_TOKEN`**: Push token for that Environment’s destination org only + - Same **name** on every Environment; different **value** per org + - Docker Hub PAT / Harbor robot token -**Custom SECRET_NAME Validation:** +**Repository secrets** (shared): -When `SECRET_NAME` is set to `custom`, the `CUSTOM_SECRET_NAME` field is **required** and validated: -- Must start with a letter or underscore -- Can only contain letters, numbers, and underscores (`[A-Za-z0-9_]`) -- No spaces, hyphens, or special characters +2. **`SLACK_WEBHOOK_DEVOPS`**: Slack notification webhook +3. **`WIREGUARD_CONFIG`**: (Optional) WireGuard VPN for private registries -| `CUSTOM_SECRET_NAME` | Valid? | -|---|---| -| `MY_ORG_DOCKER_TOKEN` | ✅ | -| `_PRIVATE_TOKEN` | ✅ | -| `my-org-token` | ❌ Hyphens not allowed | -| `MY SECRET` | ❌ Spaces not allowed | -| *(empty)* | ❌ Required when `custom` is selected | - -**How to Add Secrets:** -1. Go to GitHub repository → Settings → Secrets and variables → Actions -2. Click "New repository secret" -3. Create the secret with the exact name you will provide as `SECRET_NAME` input -4. Set the value to your Docker registry token/password +**How to Add Environment Secrets:** +1. Repo → Settings → Environments → open e.g. `transfer-qa` +2. Enable **Required reviewers** + **Prevent self-review** +3. Add Environment secret `DOCKER_TOKEN` +4. Prefer removing old repo-level `MOSIP*_DOCKER_TOKEN` secrets once this gate is live **Protected Organizations:** -Certain destination organizations (e.g., `mosipid`) are protected in the `mosip/kattu` reusable workflow. Transfers to protected organizations require **admin** access on the calling repository. This prevents accidental overwrites of production images by non-admin users. - -> **Note:** This protection is enforced in the `mosip/kattu` reusable workflow, so it cannot be bypassed by modifying the caller workflow. +Certain destination organizations (e.g., `mosipid`) are also protected in the `mosip/kattu` reusable workflow (admin-only). That remains in addition to Environment approval. **Security Benefits:** -- Tokens are never exposed in workflow logs -- Each organization has isolated credentials -- No hardcoded credentials in workflow files -- Protected organizations require admin access for transfers +- Human Approve before any push +- Tokens unlocked only after approval (Environment secrets) +- Destination org cannot be free-typed independently of the gate +- Protected organizations still require admin access in `kattu` ### Running the Workflow 1. Go to [Actions → Manual workflow to transfer images](https://github.com/mosip/release-script/actions/workflows/image-transfer.yml) 2. Click "Run workflow" -3. Select branch (usually `master` or `main`) +3. Select branch that contains the merged `images.txt` 4. Fill in the required inputs: ``` + TRANSFER_TARGET: transfer-qa USERNAME: robot$mosipdev+release-bot - SECRET_NAME: MOSIPID_DOCKER_TOKEN (select from dropdown) - CUSTOM_SECRET_NAME: (leave empty unless SECRET_NAME is "custom") - DESTINATION_ORGANIZATION: mosipid - REGISTRY_URL: https://harbor.mosip.net - REGISTRY_TYPE: harbor - ENABLE_WIREGUARD: true (if registry is on private network) - ``` -5. If using `custom` for `SECRET_NAME`, enter the secret name in `CUSTOM_SECRET_NAME`: - ``` - SECRET_NAME: custom - CUSTOM_SECRET_NAME: MY_ORG_DOCKER_TOKEN + REGISTRY_URL: https://index.docker.io/v1/ + REGISTRY_TYPE: dockerhub + ENABLE_WIREGUARD: false ``` -6. Ensure the selected/custom secret is configured under **Settings → Secrets and variables → Actions** -7. Click "Run workflow" +5. Click "Run workflow" → job waits on Environment `transfer-qa` +6. Approver: **Review deployments** → Approve (or Reject) +7. After Approve, transfer runs using that Environment’s `DOCKER_TOKEN` ### Workflow Features From 12b119ce3af961df4594aa92ba3f28dc08d8babc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 7 Sep 2026 09:33:10 +0000 Subject: [PATCH 9/9] docs: clarify Write vs merge with branch protection Explain that Write is needed to open PRs and run workflows, but merge can be blocked via branch protection, CODEOWNERS, and push restrictions. Co-authored-by: Ivanmeneges --- ...image-transfer-approval-single-workflow.md | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/release/docs/image-transfer-approval-single-workflow.md b/release/docs/image-transfer-approval-single-workflow.md index 4a130286..d297da78 100644 --- a/release/docs/image-transfer-approval-single-workflow.md +++ b/release/docs/image-transfer-approval-single-workflow.md @@ -112,7 +112,42 @@ To add a new hop later: add a choice option + one `case` arm + create the matchi |---|---|---| | Operator (PR + Run workflow) | **Write** | — | | Approver (Approve deployment) | **Read** minimum | Listed on that Environment’s required reviewers | -| Admin (create Environments / secrets) | **Admin** | — | +| Merger / Release lead | **Write** or **Maintain** | Allowed to merge by branch protection / CODEOWNERS | +| Admin (Environments / secrets) | **Admin** | — | + +### Does Write also allow merging PRs? + +**Only if the branch is unprotected.** Write includes the *capability* to merge, but **branch protection / rulesets** can block it. + +Give operators **Write** so they can: + +- Push a feature branch +- Open a PR +- Run `workflow_dispatch` + +Then lock merges with protection on `release-1.2.0.1` (and other release branches): + +| Setting | Purpose | +|---|---| +| Require a pull request before merging | No direct pushes to the release branch | +| Require approvals (1+) | Someone else must approve the PR | +| Require review from Code Owners | e.g. DevOps / leads own `release/vidivi/images.txt` | +| Do not allow bypassing the above settings | Admins cannot casually skip (or limit bypass actors) | +| Restrict who can push to matching branches | Only named Release/DevOps users if you want a hard merge gate | +| (Optional) Require conversation resolution | Forces comment cleanup before merge | + +With that: + +| Action | Operator (Write) | Approver (Read + env reviewer) | Release lead / CODEOWNER | +|---|---|---|---| +| Open PR / update `images.txt` | Yes | Optional | Yes | +| Run transfer workflow | Yes | No (Read cannot `workflow_dispatch`) | Yes | +| Approve Environment wait | No (unless also listed; block with prevent self-review) | Yes | If listed | +| Merge PR | **No** (blocked until required reviews) | Can approve PR if Write/CODEOWNER | **Yes** after review | + +So: **Write ≠ automatic merge rights** once branch protection is on. That is the standard GitHub pattern worldwide. + +**Practical setup:** Operators team = Write; Approvers for Environment = Read (or Write if they also review PRs); CODEOWNERS / “restrict who can merge” = Release/DevOps leads only. ---