44 push :
55 branches :
66 - main
7- tags :
8- - ' v*'
97 workflow_dispatch :
8+ inputs :
9+ environment :
10+ description : " Target environment for infra + app deployment"
11+ required : true
12+ type : choice
13+ default : dev
14+ options :
15+ - dev
16+ - preprod
17+ - review
1018
1119concurrency : cicd-${{ github.ref }}
1220
1321permissions :
14- contents : write
22+ contents : read
1523 id-token : write
16- attestations : write
1724 security-events : write
1825
1926jobs :
@@ -26,101 +33,144 @@ jobs:
2633 uses : ./.github/workflows/stage-2-test.yaml
2734 secrets : inherit
2835
29- release-stage :
36+ # Resolve the latest release tag once so downstream jobs can consume it.
37+ resolve :
38+ name : Resolve deployment targets
3039 needs : test-stage
31- if : github.ref == 'refs/heads/main' && github.event_name == 'push'
3240 runs-on : ubuntu-latest
3341 outputs :
34- new_release_published : ${{ steps.release.outputs.new_release_published }}
35- new_release_version : ${{ steps.release.outputs.new_release_version }}
42+ release_tag : ${{ steps.tag.outputs.release_tag }}
43+ deploy_dev : ${{ steps.envs.outputs.deploy_dev }}
44+ deploy_preprod : ${{ steps.envs.outputs.deploy_preprod }}
45+ deploy_review : ${{ steps.envs.outputs.deploy_review }}
46+ env :
47+ GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
3648 steps :
37- - name : Checkout code
38- uses : actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
39- with :
40- fetch-depth : 0
41- token : ${{ secrets.GITHUB_TOKEN }}
42-
43- - name : Read tool versions
44- id : tool-versions
49+ - name : Compute target environments
50+ id : envs
4551 run : |
46- echo "python=$(awk '/^python / {print $2}' .tool-versions)" >> "$GITHUB_OUTPUT"
47- echo "uv=$(awk '/^uv / {print $2}' .tool-versions)" >> "$GITHUB_OUTPUT"
48-
49- - name : Set up Python
50- uses : actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
51- with :
52- python-version : ${{ steps.tool-versions.outputs.python }}
53-
54- - name : Install uv
55- uses : astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
56- with :
57- version : ${{ steps.tool-versions.outputs.uv }}
58- enable-cache : true
59-
60- - name : Run semantic-release
61- id : release
62- env :
63- GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
52+ INPUT="${{ inputs.environment }}"
53+ case "$INPUT" in
54+ dev)
55+ echo "deploy_dev=true" >> "$GITHUB_OUTPUT"
56+ echo "deploy_preprod=false" >> "$GITHUB_OUTPUT"
57+ echo "deploy_review=false" >> "$GITHUB_OUTPUT"
58+ ;;
59+ preprod)
60+ echo "deploy_dev=false" >> "$GITHUB_OUTPUT"
61+ echo "deploy_preprod=true" >> "$GITHUB_OUTPUT"
62+ echo "deploy_review=false" >> "$GITHUB_OUTPUT"
63+ ;;
64+ review)
65+ echo "deploy_dev=false" >> "$GITHUB_OUTPUT"
66+ echo "deploy_preprod=false" >> "$GITHUB_OUTPUT"
67+ echo "deploy_review=true" >> "$GITHUB_OUTPUT"
68+ ;;
69+ *)
70+ # push-to-main (empty input): deploy to both dev and preprod
71+ echo "deploy_dev=true" >> "$GITHUB_OUTPUT"
72+ echo "deploy_preprod=true" >> "$GITHUB_OUTPUT"
73+ echo "deploy_review=false" >> "$GITHUB_OUTPUT"
74+ ;;
75+ esac
76+
77+ - name : Resolve latest release tag
78+ id : tag
6479 run : |
65- uv pip install python-semantic-release --system
80+ LATEST_TAG=$(gh release list \
81+ --repo "${{ github.repository }}" \
82+ --limit 1 \
83+ --json tagName --jq '.[0].tagName' 2>/dev/null || echo "")
84+ if [[ -z "$LATEST_TAG" ]]; then
85+ echo "::error::No GitHub Release exists. Push a v* tag to create the first release."
86+ exit 1
87+ fi
88+ echo "release_tag=${LATEST_TAG}" >> "$GITHUB_OUTPUT"
89+ echo "Latest release: ${LATEST_TAG}"
6690
67- # Detect next version (--print exits without side effects)
68- VERSION=$(semantic-release version --print 2>/dev/null) || true
91+ # ---- dev ------------------------------------------------------------------
6992
70- if [[ -n "$VERSION" ]] && ! git ls-remote --tags origin "refs/tags/v$VERSION" | grep -q .; then
71- echo "Next version detected: $VERSION"
93+ deploy-infra-dev :
94+ name : Deploy infra (dev)
95+ needs : resolve
96+ if : needs.resolve.outputs.deploy_dev == 'true'
97+ permissions :
98+ id-token : write
99+ uses : ./.github/workflows/stage-4-deploy.yaml
100+ with :
101+ environments : ' ["dev"]'
102+ commit_sha : ${{ github.sha }}
103+ secrets : inherit
72104
73- # Create and push the tag (no commit needed, tag push is not blocked by branch protection)
74- git tag "v$VERSION"
75- git push origin "v$VERSION"
105+ deploy-app-dev :
106+ name : Deploy app (dev)
107+ needs : [resolve, deploy-infra-dev]
108+ if : needs.resolve.outputs.deploy_dev == 'true'
109+ permissions :
110+ id-token : write
111+ uses : ./.github/workflows/stage-4-deploy-app.yaml
112+ with :
113+ environments : ' ["dev"]'
114+ release_tag : ${{ needs.resolve.outputs.release_tag }}
115+ commit_sha : ${{ github.sha }}
116+ secrets : inherit
76117
77- echo "new_release_published=true" >> "$GITHUB_OUTPUT"
78- echo "new_release_version=v$VERSION" >> "$GITHUB_OUTPUT"
79- else
80- echo "No new release needed (version: ${VERSION:-none}, tag may already exist)"
81- echo "new_release_published=false" >> "$GITHUB_OUTPUT"
82- echo "new_release_version=" >> "$GITHUB_OUTPUT"
83- fi
118+ # ---- preprod --------------------------------------------------------------
119+
120+ deploy-infra-preprod :
121+ name : Deploy infra (preprod)
122+ needs : [resolve, deploy-app-dev]
123+ if : |
124+ always() &&
125+ needs.resolve.outputs.deploy_preprod == 'true' &&
126+ (needs.deploy-app-dev.result == 'success' || needs.deploy-app-dev.result == 'skipped')
127+ permissions :
128+ id-token : write
129+ uses : ./.github/workflows/stage-4-deploy.yaml
130+ with :
131+ environments : ' ["preprod"]'
132+ commit_sha : ${{ github.sha }}
133+ secrets : inherit
84134
85- build-stage :
86- needs : [test-stage, release-stage]
135+ deploy-app-preprod :
136+ name : Deploy app (preprod)
137+ needs : [resolve, deploy-infra-preprod]
87138 if : |
88139 always() &&
89- needs.test-stage.result == 'success' &&
90- (
91- github.ref_type == 'tag' ||
92- needs.release-stage.outputs.new_release_published == 'true'
93- )
94- uses : ./.github/workflows/stage-3-build.yaml
140+ needs.resolve.outputs.deploy_preprod == 'true' &&
141+ needs.deploy-infra-preprod.result == 'success'
142+ permissions :
143+ id-token : write
144+ uses : ./.github/workflows/stage-4-deploy-app.yaml
95145 with :
96- version : ${{ needs.release-stage.outputs.new_release_version }}
97- new_release_published : ${{ needs.release-stage.outputs.new_release_published == 'true' }}
146+ environments : ' ["preprod"]'
147+ release_tag : ${{ needs.resolve.outputs.release_tag }}
148+ commit_sha : ${{ github.sha }}
98149 secrets : inherit
99150
100- deploy-stage :
101- name : Deploy stage
102- needs : [commit-stage, test-stage]
151+ # ---- review (manual only) -------------------------------------------------
152+
153+ deploy-infra-review :
154+ name : Deploy infra (review)
155+ needs : resolve
156+ if : needs.resolve.outputs.deploy_review == 'true'
103157 permissions :
104158 id-token : write
105159 uses : ./.github/workflows/stage-4-deploy.yaml
106160 with :
107- environments : ' ["review", "dev", "preprod" ]'
161+ environments : ' ["review"]'
108162 commit_sha : ${{ github.sha }}
109163 secrets : inherit
110164
111- deploy-app-stage :
112- name : Deploy app stage
113- needs : [build-stage, release-stage]
114- # Only deploy when a new release was published — no release means no deployable artifact.
115- if : |
116- always() &&
117- needs.build-stage.result == 'success' &&
118- needs.release-stage.outputs.new_release_published == 'true'
165+ deploy-app-review :
166+ name : Deploy app (review)
167+ needs : [resolve, deploy-infra-review]
168+ if : needs.resolve.outputs.deploy_review == 'true'
119169 permissions :
120170 id-token : write
121171 uses : ./.github/workflows/stage-4-deploy-app.yaml
122172 with :
123- environments : ' ["dev", "preprod "]'
124- release_tag : ${{ needs.release-stage .outputs.new_release_version }}
173+ environments : ' ["review "]'
174+ release_tag : ${{ needs.resolve .outputs.release_tag }}
125175 commit_sha : ${{ github.sha }}
126176 secrets : inherit
0 commit comments