How-To: Promote a Release (dev -> main)
In EPURE, dev is the integration branch and main is the release line. Neither developers nor the Agent App push to main: only the Release App (<owner>-epure-release) does, from the release workflow, inside the protected release environment.
Promoting a release is the 4th Mandatory Human Validation Gate (Q101).
Both entry points run the same implementation, the reusable workflow Giskard-AI/epure/.github/workflows/release-promote.yml:
| Repository | Workflow to dispatch | Rulesets that must list the Release App as bypass actor |
|---|---|---|
Consumer repository bootstrapped by /setup-repo | .github/workflows/release.yml (EPURE Controlled Release (dev -> main)), a thin caller pinned to an EPURE commit SHA | epure-release-immutability |
Giskard-AI/epure itself | .github/workflows/release-promote.yml (Promote Dev to Main (Fast-Forward Release)) | Main basic protection, epure-branch-governance, epure-tag-immutability |
Inputs: semver_tag (vMAJOR.MINOR.PATCH), expected_dev_sha (the full 40-character SHA of the dev head you reviewed) and, on release-promote.yml, the optional ci_workflow (default ci.yml, the workflow file whose run on dev must have succeeded).
1. One-Time Setup
Create and install the Release App. Stage 3 of
scripts/wizards/setup-github-apps-wizard.sh --repo <owner>/<repo>creates<owner>-epure-release(repository permissionsContents: Read and write,Metadata: Read-only, no webhook), installs it on that repository only, and stores two secrets in thereleaseenvironment:EPURE_RELEASE_APP_ID(numeric App ID) andEPURE_RELEASE_APP_PRIVATE_KEY(PEM). The wizard always runs all stages, including the Agent App; if you only need the Release App, follow the same steps by hand and set the secrets withgh secret set <NAME> -R <owner>/<repo> --env release.Add the Release App as bypass actor (
actor_type: Integration,actor_id: <App ID>,bypass_mode: always) on every ruleset that coversmainandv*tags (see the table above). In consumer repositories,node dist/cli/setup-repo.mjs --repo <owner>/<repo> --release-app-id <App ID> --apply --apply-github-settingsdoes it forepure-release-immutability. Elsewhere, add the actor withgh apiwhile keeping the existing actors:bashgh api repos/<owner>/<repo>/rulesets/<ruleset-id> \ | jq '{name, target, enforcement, conditions, rules, bypass_actors: (.bypass_actors + [{actor_id: <App ID>, actor_type: "Integration", bypass_mode: "always"}])}' \ | gh api -X PUT repos/<owner>/<repo>/rulesets/<ruleset-id> --input -Protect the
releaseenvironment with at least one required reviewer and a deployment branch policy that only allowsdev:bashgh api -X PUT repos/<owner>/<repo>/environments/release --input - <<'EOF' { "reviewers": [{"type": "User", "id": <reviewer-user-id>}], "deployment_branch_policy": {"protected_branches": false, "custom_branch_policies": true} } EOF gh api -X POST repos/<owner>/<repo>/environments/release/deployment-branch-policies \ -f name=dev -f type=branch
2. Prerequisites Before Each Release
- All target
feature/* -> devPull Requests have been squash-merged intodev. - The
CIworkflow (.github/workflows/ci.yml) concludedsuccesson thedevhead. - You picked the next tag from the Conventional Commits since the previous tag (
fix:-> PATCH,feat:-> MINOR,!:/BREAKING CHANGE-> MAJOR).
3. Trigger the Release
Dispatch the workflow from dev (the release environment rejects other refs), then approve the pending deployment in the Actions tab.
Consumer repository:
DEV_SHA="$(gh api repos/my-org/craft-calculator/branches/dev --jq .commit.sha)"
gh workflow run release.yml -R my-org/craft-calculator --ref dev \
-f semver_tag=v1.0.0 -f expected_dev_sha="$DEV_SHA"EPURE itself:
DEV_SHA="$(gh api repos/Giskard-AI/epure/branches/dev --jq .commit.sha)"
gh workflow run release-promote.yml -R Giskard-AI/epure --ref dev \
-f semver_tag=v1.0.0 -f expected_dev_sha="$DEV_SHA"4. What release-promote.yml Executes
- Validates before any write and aborts with an
::error::annotation when:semver_tagdoes not matchvMAJOR.MINOR.PATCH(no leading zeros);expected_dev_shais not a 40-character SHA equal to the currentorigin/devhead;- the tag already exists;
origin/mainis not an ancestor ofexpected_dev_sha(no fast-forward possible);- the latest
CIrun for that SHA ondevdid not concludesuccess; EPURE_RELEASE_APP_IDorEPURE_RELEASE_APP_PRIVATE_KEYis missing from thereleaseenvironment.
- Verifies Release App role separation with
actions/verify-app-role(promote_dev_to_mainonrefs/heads/main, fromrelease.yml/release-promote.yml, environmentrelease). - Mints a short-lived installation token with
actions/create-github-app-token, scoped to this repository withcontents: writeonly. - Fast-forwards
mainand tags atomically: creates the annotated tag onexpected_dev_shaas<owner>-epure-release[bot], then runs onegit push --atomicof<sha>:refs/heads/mainandrefs/tags/<tag>. Both refs move or neither does; nothing is force-pushed, and the token is passed togitthrough the process environment only (never written to.git/config). - Publishes the GitHub Release with
gh release create --verify-tag --generate-notes --notes-start-tag <previous tag> --latest. - Revokes the token in an
always()step, even when an earlier step failed.
5. Re-Running After a Failure
Re-dispatch with the same inputs:
- Failure before the push: nothing was written; fix the cause and re-run.
- Push done, release not published: the workflow detects that the tag and
origin/mainalready point toexpected_dev_sha, skips the push (and thedevhead check, sincedevmay have moved), and only publishes the GitHub Release. - Release already published: the run ends with a notice and changes nothing.
- Tag exists on another commit: the run aborts. Release tags are immutable; choose the next version instead.

