Skip to content

Commit cf73be5

Browse files
committed
chore: add force deploy confirmation and validate PR status in deployment workflow
- Introduced `force_deploy_confirmation` input to allow bypassing all PR checks during deployment. - Added a new job to check for force deploy mode and validate PR status, ensuring that deployments only proceed if PRs are ready for merging unless forced. - Enhanced logging for better visibility during the deployment process.
1 parent 8e117c6 commit cf73be5

1 file changed

Lines changed: 122 additions & 55 deletions

File tree

.github/workflows/deploy-beta.yml

Lines changed: 122 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ on:
77
description: "Deploy to NPM"
88
type: boolean
99
default: false
10+
force_deploy_confirmation:
11+
description: "Force deploy (bypass ALL PR checks). Type 'FORCE_DEPLOY' to confirm"
12+
type: string
13+
default: ""
14+
required: false
1015

1116
concurrency:
1217
group: ${{ github.workflow }}-${{ github.ref }}
@@ -30,15 +35,14 @@ jobs:
3035
secrets:
3136
PAT: ${{ secrets.PAT }}
3237

33-
get-deploy-inputs:
34-
name: Get Deploy Inputs
38+
find-pr:
39+
name: Find PR for Current Branch
3540
needs: validate-actor
3641
runs-on: [self-hosted, Linux, X64]
3742
outputs:
38-
version_suffix: ${{ steps.deploy-inputs.outputs.version_suffix }}
39-
beta_identifier: ${{ steps.deploy-inputs.outputs.beta_identifier }}
40-
beta_identifier_for_automation_tests: ${{ steps.deploy-inputs.outputs.beta_identifier_for_automation_tests }}
41-
trigger_source: ${{ format('Triggered via PR <{0}|#{1}> by <{2}|{3}>', steps.pr-info.outputs.pr_url, steps.pr-info.outputs.pr_number, format('{0}/{1}', github.server_url, github.actor), github.actor) }}
43+
pr_number: ${{ steps.pr-info.outputs.pr_number }}
44+
pr_url: ${{ steps.pr-info.outputs.pr_url }}
45+
force_deploy_mode: ${{ steps.check-force-deploy.outputs.force_deploy_mode }}
4246
steps:
4347
- name: Harden the runner (Audit all outbound calls)
4448
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
@@ -50,76 +54,139 @@ jobs:
5054
with:
5155
fetch-depth: 0
5256

57+
- name: Check force deploy mode
58+
id: check-force-deploy
59+
run: |
60+
if [ "${{ inputs.force_deploy_confirmation }}" = "FORCE_DEPLOY" ]; then
61+
echo "force_deploy_mode=true" >> $GITHUB_OUTPUT
62+
echo "⚠️ FORCE DEPLOY MODE ENABLED - All PR validation checks will be skipped"
63+
else
64+
echo "force_deploy_mode=false" >> $GITHUB_OUTPUT
65+
fi
66+
5367
- name: Find PR for current branch
5468
id: pr-info
5569
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
5670
with:
5771
script: |
5872
const branch = context.ref.replace('refs/heads/', '');
5973
console.log(`Finding PRs for branch: ${branch}`);
60-
74+
6175
const { data: pullRequests } = await github.rest.pulls.list({
6276
owner: context.repo.owner,
6377
repo: context.repo.repo,
6478
head: `${context.repo.owner}:${branch}`,
6579
base: 'develop',
6680
state: 'open'
6781
});
68-
82+
6983
if (pullRequests.length > 0) {
7084
const pr = pullRequests[0];
7185
const prNumber = pr.number.toString();
7286
const prUrl = pr.html_url;
73-
console.log(`Found PR #${prNumber} for branch ${branch}: https://github.com/${context.repo.owner}/${context.repo.repo}/pull/${prNumber}`);
74-
75-
// Check if PR is in draft state
76-
if (pr.draft) {
77-
core.setFailed(`PR #${prNumber} is in draft state. Please mark it as ready for review before deploying.`);
78-
return;
79-
}
80-
81-
// Get detailed PR information including mergeable state
82-
const { data: prDetails } = await github.rest.pulls.get({
83-
owner: context.repo.owner,
84-
repo: context.repo.repo,
85-
pull_number: prNumber
86-
});
87-
88-
// Check if PR is mergeable and all requirements are satisfied
89-
if (prDetails.mergeable === false) {
90-
core.setFailed(`PR #${prNumber} is not in a mergeable state. Please resolve conflicts before deploying.`);
91-
return;
92-
}
93-
94-
// The mergeable_state can be one of: clean, dirty, blocked, unstable, or unknown
95-
// Only 'clean' means all requirements are met (checks passed, approvals received, no conflicts)
96-
if (prDetails.mergeable_state !== 'clean') {
97-
// Get more details about why it's not clean
98-
let reason = '';
99-
100-
if (prDetails.mergeable_state === 'blocked') {
101-
reason = 'required checks or approvals are missing';
102-
} else if (prDetails.mergeable_state === 'dirty') {
103-
reason = 'there are merge conflicts';
104-
} else if (prDetails.mergeable_state === 'unstable') {
105-
reason = 'required checks are failing';
106-
} else {
107-
reason = `the mergeable state is "${prDetails.mergeable_state}"`;
108-
}
109-
110-
core.setFailed(`PR #${prNumber} is not ready to merge: ${reason}. Please resolve all issues before deploying.`);
111-
return;
112-
}
113-
114-
console.log(`PR #${prNumber} is in a clean mergeable state. All requirements satisfied. Proceeding with beta deployment.`);
115-
87+
console.log(`Found PR #${prNumber} for branch ${branch}: ${prUrl}`);
88+
11689
core.setOutput('pr_number', prNumber);
11790
core.setOutput('pr_url', prUrl);
11891
} else {
11992
core.setFailed(`No open PR found for branch ${branch} targeting develop branch`);
120-
core.setOutput('pr_number', '');
12193
}
12294
95+
validate-pr:
96+
name: Validate PR Status
97+
needs: find-pr
98+
if: ${{ needs.find-pr.outputs.force_deploy_mode != 'true' }}
99+
runs-on: [self-hosted, Linux, X64]
100+
steps:
101+
- name: Harden the runner (Audit all outbound calls)
102+
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
103+
with:
104+
egress-policy: audit
105+
106+
- name: Checkout code
107+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
108+
with:
109+
fetch-depth: 0
110+
111+
- name: Validate PR
112+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
113+
with:
114+
script: |
115+
const prNumber = '${{ needs.find-pr.outputs.pr_number }}';
116+
117+
console.log(`Validating PR #${prNumber}...`);
118+
119+
// Get detailed PR information
120+
const { data: prDetails } = await github.rest.pulls.get({
121+
owner: context.repo.owner,
122+
repo: context.repo.repo,
123+
pull_number: prNumber
124+
});
125+
126+
// Check if PR is in draft state
127+
if (prDetails.draft) {
128+
core.setFailed(`PR #${prNumber} is in draft state. Please mark it as ready for review before deploying.`);
129+
return;
130+
}
131+
132+
// Check if PR is mergeable
133+
if (prDetails.mergeable === false) {
134+
core.setFailed(`PR #${prNumber} is not in a mergeable state. Please resolve conflicts before deploying.`);
135+
return;
136+
}
137+
138+
// Check mergeable_state
139+
if (prDetails.mergeable_state !== 'clean') {
140+
let reason = '';
141+
142+
if (prDetails.mergeable_state === 'blocked') {
143+
reason = 'required checks or approvals are missing';
144+
} else if (prDetails.mergeable_state === 'dirty') {
145+
reason = 'there are merge conflicts';
146+
} else if (prDetails.mergeable_state === 'unstable') {
147+
reason = 'required checks are failing';
148+
} else {
149+
reason = `the mergeable state is "${prDetails.mergeable_state}"`;
150+
}
151+
152+
core.setFailed(`PR #${prNumber} is not ready to merge: ${reason}. Please resolve all issues before deploying.`);
153+
return;
154+
}
155+
156+
console.log(`PR #${prNumber} is in a clean mergeable state. All requirements satisfied.`);
157+
158+
get-deploy-inputs:
159+
name: Get Deploy Inputs
160+
needs: [find-pr, validate-pr]
161+
if: ${{ always() && needs.find-pr.result == 'success' && (needs.validate-pr.result == 'success' || needs.validate-pr.result == 'skipped') }}
162+
runs-on: [self-hosted, Linux, X64]
163+
outputs:
164+
version_suffix: ${{ steps.deploy-inputs.outputs.version_suffix }}
165+
beta_identifier: ${{ steps.deploy-inputs.outputs.beta_identifier }}
166+
beta_identifier_for_automation_tests: ${{ steps.deploy-inputs.outputs.beta_identifier_for_automation_tests }}
167+
trigger_source: ${{ steps.set-trigger-source.outputs.trigger_source }}
168+
steps:
169+
- name: Harden the runner (Audit all outbound calls)
170+
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
171+
with:
172+
egress-policy: audit
173+
174+
- name: Checkout code
175+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
176+
with:
177+
fetch-depth: 0
178+
179+
- name: Set trigger source
180+
id: set-trigger-source
181+
run: |
182+
TRIGGER_BASE="Triggered via PR <${{ needs.find-pr.outputs.pr_url }}|#${{ needs.find-pr.outputs.pr_number }}> by <${{ github.server_url }}/${{ github.actor }}|${{ github.actor }}>"
183+
184+
if [ "${{ needs.find-pr.outputs.force_deploy_mode }}" = "true" ]; then
185+
echo "trigger_source=${TRIGGER_BASE} [⚠️ FORCE DEPLOY]" >> $GITHUB_OUTPUT
186+
else
187+
echo "trigger_source=${TRIGGER_BASE}" >> $GITHUB_OUTPUT
188+
fi
189+
123190
- name: Extract deploy inputs
124191
id: deploy-inputs
125192
shell: bash
@@ -130,9 +197,9 @@ jobs:
130197
# - "beta.pr." is a fixed prefix indicating a beta release for a pull request.
131198
# - <PR_NUMBER> is the number of the pull request associated with the branch.
132199
# - <SHORT_COMMIT_HASH> is the first 7 characters of the commit hash for traceability.
133-
echo "version_suffix=beta.pr.${{ steps.pr-info.outputs.pr_number }}.$SHA_SHORT" >> $GITHUB_OUTPUT
134-
echo "beta_identifier=PR-${{ steps.pr-info.outputs.pr_number }}/$SHA_SHORT" >> $GITHUB_OUTPUT
135-
echo "beta_identifier_for_automation_tests=pr-${{ steps.pr-info.outputs.pr_number }}-$SHA_SHORT" >> $GITHUB_OUTPUT
200+
echo "version_suffix=beta.pr.${{ needs.find-pr.outputs.pr_number }}.$SHA_SHORT" >> $GITHUB_OUTPUT
201+
echo "beta_identifier=PR-${{ needs.find-pr.outputs.pr_number }}/$SHA_SHORT" >> $GITHUB_OUTPUT
202+
echo "beta_identifier_for_automation_tests=pr-${{ needs.find-pr.outputs.pr_number }}-$SHA_SHORT" >> $GITHUB_OUTPUT
136203
137204
deploy-cdn:
138205
name: Deploy to CDN

0 commit comments

Comments
 (0)