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
1116concurrency :
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