---
name: celerity-deploy-release
description: 'Use when: the user says "do the release", "deploy ", "release management", or names a fruit / DL version to ship for a Celerity / SupportGoWhere-style repo set. Provides the end-to-end deploy + release process: release numbering, Jira release, GitLab release branch + pipelines, security scans, Confluence release doc, deploy, and the GitLab release.'
user-invocable: true
---
# Skill: Celerity Deploy + Release
End-to-end deploy + release process for a Celerity-style repo set (a web
frontend, an API backend, and a CMS). It is a runbook, not an automation: the
agent drives Jira, GitLab, and Confluence steps in order and pauses at the
inherently-manual gates (clicking deploy in the pipeline UI, file scans).
> **Configure before first use.** This skill was generalised from an internal
> SupportGoWhere runbook. The project-specific identifiers (Jira project + version
> IDs, GitLab project IDs, Confluence space + page IDs, tooling hostnames) are
> shown as ``. Fill them in from your own project, ideally via the
> environment variables listed in **Configuration** below, before running any
> step. Do not commit real internal IDs back to a public plugin.
## Integrations (MCP-first)
This skill reaches Jira, Confluence, and GitLab through **MCP servers**, not
CLIs. It calls at the capability level (`jira.*`, `confluence.*`, `gitlab.*`), so
it runs against whichever approved server(s) your environment provides; the REST
shapes in the table below are kept only as a reference for what each capability
maps to. Only `git` and `psql` remain as local commands.
On the GovTech managed setup a **single** server provides all three systems.
Declare just it:
```jsonc
{
"govtech-mcp": {
"url": "https://mcp-gw.seed.tech.gov.sg/mcp",
"type": "http"
}
}
```
Off that setup, declare whatever approved servers cover the three systems (e.g.
an Atlassian MCP for Jira + Confluence and a GitLab-native MCP). Auth is handled
by the server, not by this skill. Confirm the tool names your server exposes and
use those; the `jira.*` / `confluence.*` / `gitlab.*` labels below are generic
names for "the tool that does X". Full setup: `references/mcp-setup.md`.
Set these project values once (shell env, a local `.env`, or MCP config):
| Variable | Meaning | Example shape |
| --- | --- | --- |
| `JIRA_PROJECT_KEY` | Jira project key for releases | `ABC` |
| `JIRA_PROJECT_ID` | Numeric Jira project id | `10000` |
| `GITLAB_HOST` | GitLab host | `gitlab.example.com` |
| `GITLAB_PID_WEB` / `GITLAB_PID_API` / `GITLAB_PID_CMS` | GitLab numeric project ids | `12345` |
| `CONFLUENCE_SPACE` | Confluence space key for release docs | `RELEASES` |
| `CONFLUENCE_RELEASES_PARENT_ID` | Parent page id (" Releases") | `` |
| `CONFLUENCE_TRACKER_ID` | Combined Release Management tracker page id | `` |
| `CONFLUENCE_SCAN_LOG_ID` | OSS / SAST Reports Log page id | `` |
The two Confluence guides that this process must stay consistent with (the
Release Management Guide and the Combined Release Management tracker) live in
your project's Confluence space; link them at the top of your own copy.
### Capability ā MCP tool ā REST reference
| Capability | MCP server | REST shape it maps to (reference only) |
| --- | --- | --- |
| `jira.listVersions` | Atlassian/Jira | `GET /rest/api/3/project//versions` |
| `jira.renameVersion` / `jira.createVersion` | Atlassian/Jira | `PUT` / `POST /rest/api/3/version/` |
| `jira.searchByFixVersion` | Atlassian/Jira | `GET /rest/api/3/search/jql?jql=fixVersion=` |
| `jira.releaseVersion` | Atlassian/Jira | `PUT /rest/api/3/version/` `{released,releaseDate}` |
| `confluence.getPage` (storage) | Atlassian/Confluence | `GET /rest/api/content/?expand=body.storage,version` |
| `confluence.createPage` / `confluence.updatePage` | Atlassian/Confluence | `POST` / `PUT /rest/api/content` |
| `gitlab.listPipelines` | GitLab | `GET /projects//pipelines?ref=` |
| `gitlab.getPipelineJobs` | GitLab | `GET /projects//pipelines//jobs` |
| `gitlab.createRelease` | GitLab | `POST /projects//releases` |
If a tool for a capability is not exposed by your MCP server, fall back to the
REST shape via whatever authenticated transport your environment allows, and log
the gap so it can be raised for the catalogue.
## Concepts (read first)
- **Release number** (e.g. 178): canonical, incremental, monotonic. The primary
identifier: it titles the Confluence doc (` Release `) and leads
the tracker's `Release no./Fruit` cell. A hotfix takes the next number (a fruit
hotfixing 177 = 178).
- **Fruit** (e.g. Rose Apple, Marionberry): ephemeral mnemonic for a release,
secondary to the number. Lives in the Jira version name and the tracker cell
(under the number), NOT the doc title. Fruits may repeat if previously
discarded, so "Apple" (177) and "Rose Apple" (178) are distinct fruits.
- **Release version**: repo-specific, semver-ish (web `v8.69.2`, api `v2.44.0`,
cms `v4.27.0`). Watch for a repo whose API package is renamed to avoid a clash
with the CMS's own api package.
- Gitflow with **only `master`** (no develop). The release number is NOT the
`release/vā¦` branch name.
- Map fruit <-> version(s) via Jira releases. If you cannot map, ASK.
## Prerequisites
1. Work tagged with Jira "Affected version" + "Fix version" as it goes (create
versions following semver as needed).
2. Work is Done.
## Step 1: Choose release number + fruit name
- **Release number**: the next monotonic integer after the last released number
(check the tracker / recent doc titles; e.g. 177 -> 178). Titles the Confluence
doc.
- **Fruit**: pick one NOT currently in use in Jira releases (a long-discarded one
is fine to reuse). Verify with `jira.listVersions` on `JIRA_PROJECT_KEY` and
scan the names.
## Step 2: Jira release
Create (or, if a matching version already exists, **rename**) the Jira version to
`[] ` form, e.g. `[Rose Apple] Digital Lobby 1.1.1`.
- Rename with `jira.renameVersion` (set the new `name`), or `jira.createVersion`
if none exists.
- Find issues in a version with `jira.searchByFixVersion` (a JQL
`fixVersion=` search; the old non-JQL search endpoint is deprecated and
returns empty).
## Step 3: GitLab release branch + pipelines
- **Branch naming is versioned, NOT fruit**: `release/v` (e.g.
`release/v8.69.2`). The fruit lives in Jira/Confluence, not the branch.
- Branch from **master** where possible, OR from the **previous release commit**
when isolating a hotfix.
- **Hotfix pattern (important):** branch from the previous release's tagged
commit, then **cherry-pick** the specific fix commit(s). Do NOT branch directly
from the fix commit on master if master has later unscheduled commits:
```bash
# = tag of the release you are patching.
# = the commit to ship.
WT=/tmp/rel--web
git worktree add -b release/v $WT
git -C $WT cherry-pick # keeps original author
git -C $WT push -u origin release/v # triggers pipeline
git worktree remove $WT --force
```
Always use a worktree, never the main checkout.
- Tags: the web repo uses **bare** `X.Y.Z` tags (no `v`); branches use
`release/vX.Y.Z`. With Lerna independent-versioning (root `package.json` at
`0.0.0`), the release version comes from the tag/branch name.
- Pushing a `release/*` branch triggers CI. Typically `release/*` and `master`
get build + UAT-track deploy jobs; prod jobs are `when: manual`. Deploys to UAT
unless instructed otherwise. Release ALL packages unless told otherwise.
- Check a pipeline with `gitlab.listPipelines` on `GITLAB_PID_WEB` filtered by
`ref=` (take the most recent). Get its failed jobs with
`gitlab.getPipelineJobs`.
- When UAT deploy is green, run regression on UAT (async; QE may own it). Asking a
person is an external message in the user's identity: get exact wording approval
first, do not auto-send.
## Step 4: Security scans (skip if already done for the base release today)
Per release, before release: SAST (e.g. Fortify) + OSS/dependency scan (e.g.
Nexus IQ), results into the OSS/SAST Reports Log (`CONFLUENCE_SCAN_LOG_ID`).
Report name `YYYYMMDD-reponame`, "Static Issue Detail" template, PDF. If your
SAST create call is non-blocking, loop then poll.
## Step 5: Confluence release document
- Clone the most recent comparable release doc (a single-repo release is the
cleanest template). Space `CONFLUENCE_SPACE`, parent
`CONFLUENCE_RELEASES_PARENT_ID` (" Releases"). **Title by release NUMBER:
` Release `.** Only use a fruit-titled doc as a fallback when no
number is assigned yet; rename it once the number is allocated.
- Procedure that works: read the model doc's storage XHTML with
`confluence.getPage` (storage body), do targeted string replacements
(description/tickets, Jira `versions/`, version-change cells prev/new,
branch, pipeline id), then `confluence.createPage` the new doc.
- GOTCHA: version-change cells are `vX.Y.Z
`. Replace the **new-version**
value before the **previous-version** value (or use ordered placeholders) so
you don't collapse both cells to the same number.
- Fill: which repos to deploy, pre/deploy/post details, db migrations if appgen,
release branch, pipelines, Jira release link, Jira tickets + descriptions.
### Step 5b: Add a row to the Combined Release Management tracker
The master tracker (`CONFLUENCE_TRACKER_ID`) is an 8-column table:
`Release no./Fruit | Year | Release Date | Features | Release document | PIC |
Versioning | Status`. Add a row in date order (a hotfix goes right after the
release it patches).
- Read the tracker storage XHTML with `confluence.getPage` on
`CONFLUENCE_TRACKER_ID`, insert a `` right after the closing `
` of the
row you're following, and write it back with `confluence.updatePage`
(`version.number + 1`).
- Cell 1 holds the release number AND fruit on separate `` lines; features as
`REPO` + `
- ` ticket lines; release-doc cell links the
Confluence doc; versioning like `web v8.69.2`.
- Status uses a `status` structured-macro: `RELEASED`/`Green` when live,
`IN PROGRESS`/`Blue` while deploying, `PLANNING`/`Purple` when future.
- Leave PIC empty unless told who it is (do not assert an identity unasked).
## Step 6: Deploy
**CRITICAL (web): the deploy must run from a UI ("Run pipeline") pipeline, NOT
the auto push pipeline.** Deploy jobs are typically gated
`if: $CI_PIPELINE_SOURCE == "web" && $DEPLOY_ == "true"`. A `push` pipeline
(auto-triggered when you push `release/*`) contains NO deploy jobs. The "Run
pipeline" UI preselects the deploy vars to `"false"`; set the right one(s) to
`"true"`, e.g.:
- `DEPLOY_WEB`: main web app
- `DEPLOY_DIGITAL_LOBBY`: the kiosk app (easy to miss when shipping a DL release)
- `DEPLOY_DIGITAL_LOBBY_SCANNER`: the softcopy-upload scanner app
- `RUN_SECURITY_SCANS`: official SAST + OSS scan on a manual pipeline
Flow: Build > Pipelines > Run pipeline > pick `release/v` > flip the
needed `DEPLOY_*` var(s) to `true` > Run. Prod jobs are then `when: manual` (press
the buttons). UAT deploy auto-runs in that web pipeline once the build passes.
Ensure the image-push job completed before deploy; on a registry 403, rerun the
get-token job. DB migrations are manual on the deploy tooling server
(`cat file.sql | psql -h -p -U -d `).
## Step 6b: Post-deploy verification (smoke test + tick release-doc checkboxes)
After prod is live, run the **[[smoke-test-sgw]]** skill against the deployed
sites (routes enumerated from the codebase, not guessed). A green run (0 failing;
AUTH-GATE and clean 404 both count as PASS) lets you tick the release doc's
"Deployment verification" checkboxes that smoke actually proves: availability +
render only. Do NOT tick checks that bundle interactive QE (Singpass login,
happy-flows, MyInfo, file upload, accessibility scan); leave those for the human
QE pass. Checkboxes are `` items: flip
`incomplete` to `complete` via storage-edit + PUT
(`version + 1`). When filling a "Verified by" cell from an automated run, prefix
it `Agent:`, never assert a human verified it.
## Step 7: GitLab release
On release day (or backdate `released_at` to the Jira/Confluence date): use
`gitlab.createRelease` on the existing tag, blank/default title (= tag), notes
from the Jira fix version, links to the Jira release + Confluence doc. Then mark
the Jira fix version released with `jira.releaseVersion` (`released: true`,
`releaseDate: YYYY-MM-DD`).
## Worked example (shape, IDs redacted)
Release **178** (hotfix following 177), fruit "Rose Apple", single-repo web only:
- Jira version renamed `Digital Lobby 1.1.1` -> `[Rose Apple] Digital Lobby 1.1.1`.
- Branched `release/v8.69.2` from the 177 tag commit, cherry-picked the one fix
commit, pushed.
- **Deploy gotcha hit:** the push pipeline carried no DL deploy jobs; deploy needed
a UI "Run pipeline" with `DEPLOY_DIGITAL_LOBBY=true`. The API cannot produce a
`source: web` pipeline.
- Confluence doc cloned from Release 177, titled " Release 178" once the
number was assigned; tracker row shows `#178 / ROSE APPLE`.
- Scans skipped (done for 177 the same day). Tag `8.69.2` + GitLab release created,
Jira version marked released, tracker row -> RELEASED/Green, smoke test green ->
ticked "Tagged" + CMS checks.