Skip to content

Commit 1e2a39c

Browse files
authored
Added ephemeral staging environments (#432)
closes https://linear.app/ghost/issue/AP-976 - Create ephemeral staging environment on PR labels *.ghost.is
1 parent 74fa2b2 commit 1e2a39c

File tree

1 file changed

+139
-4
lines changed

1 file changed

+139
-4
lines changed
Lines changed: 139 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1-
name: CI
1+
name: CICD
22

33
on:
44
workflow_dispatch:
55
pull_request:
6+
types:
7+
- opened
8+
- synchronize
9+
- reopened
10+
- labeled
11+
- unlabeled
612
push:
713
branches:
814
- main
@@ -42,7 +48,7 @@ jobs:
4248

4349
build-test-push:
4450
name: Build, Test and Push
45-
environment: staging
51+
environment: build
4652
runs-on: ubuntu-latest
4753
needs: [lint, check-yarn-lock]
4854
outputs:
@@ -100,6 +106,7 @@ jobs:
100106
run: yarn test
101107

102108
- name: "Authenticate with GCP"
109+
if: github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.action == 'reopened' || github.event.action == 'labeled' || github.event.action == 'unlabeled')
103110
id: gcp-auth
104111
uses: google-github-actions/auth@v2
105112
with:
@@ -108,20 +115,23 @@ jobs:
108115
service_account: [email protected]
109116

110117
- name: "Login to GCP Artifact Registry"
118+
if: github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.action == 'reopened' || github.event.action == 'labeled' || github.event.action == 'unlabeled')
111119
uses: docker/login-action@v3
112120
with:
113121
registry: europe-docker.pkg.dev
114122
username: oauth2accesstoken
115123
password: ${{ steps.gcp-auth.outputs.access_token }}
116124

117125
- name: "Push ActivityPub Docker Image"
126+
if: github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.action == 'reopened' || github.event.action == 'labeled' || github.event.action == 'unlabeled')
118127
uses: docker/build-push-action@v6
119128
with:
120129
context: .
121130
push: true
122131
tags: ${{ steps.activitypub-docker-metadata.outputs.tags }}
123132

124133
- name: "Push Migrations Docker Image"
134+
if: github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.action == 'reopened' || github.event.action == 'labeled' || github.event.action == 'unlabeled')
125135
uses: docker/build-push-action@v6
126136
with:
127137
context: migrate
@@ -135,10 +145,136 @@ jobs:
135145
env:
136146
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
137147

148+
deploy-pr:
149+
if: github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.action == 'reopened' || github.event.action == 'labeled' || github.event.action == 'unlabeled')
150+
name: (ephemeral staging) Deploy
151+
runs-on: ubuntu-latest
152+
needs: [build-test-push]
153+
environment: staging
154+
steps:
155+
- name: "Check if any label matches *.ghost.is"
156+
id: check-labels
157+
env:
158+
LABELS: ${{ toJson(github.event.pull_request.labels) }}
159+
run: |
160+
export LABEL_NAMES=$(echo "$LABELS" | jq -r '[.[] | select(.name | test("\\.ghost\\.is$")) | .name] | join(",")')
161+
echo "Label names: $LABEL_NAMES"
162+
if [ "$LABEL_NAMES" != "" ]; then
163+
echo "Label matching *.ghost.is found."
164+
echo "is_ephemeral_staging=true" >> "$GITHUB_OUTPUT"
165+
else
166+
echo "No label matching .*.ghost.is found."
167+
echo "is_ephemeral_staging=false" >> "$GITHUB_OUTPUT"
168+
fi
169+
170+
- name: "Checkout activitypub-infra repo"
171+
if: ${{ steps.check-labels.outputs.is_ephemeral_staging == 'true' }}
172+
uses: actions/checkout@v4
173+
with:
174+
repository: TryGhost/activitypub-infra
175+
ssh-key: ${{ secrets.ACTIVITYPUB_INFRA_DEPLOY_KEY }}
176+
path: activitypub-infra
177+
178+
- name: "Checkout terraform repo"
179+
if: ${{ steps.check-labels.outputs.is_ephemeral_staging == 'true' }}
180+
uses: actions/checkout@v4
181+
with:
182+
repository: TryGhost/terraform
183+
ssh-key: ${{ secrets.TERRAFORM_DEPLOY_KEY }}
184+
path: terraform
185+
186+
- name: "Get terraform version"
187+
if: ${{ steps.check-labels.outputs.is_ephemeral_staging == 'true' }}
188+
id: terraform-version
189+
run: |
190+
echo "terraform_version=$(cat activitypub-infra/infrastructure/activitypub-staging-environments/.terraform-version)" >> "$GITHUB_OUTPUT"
191+
192+
- name: "Setup terraform"
193+
if: ${{ steps.check-labels.outputs.is_ephemeral_staging == 'true' }}
194+
uses: hashicorp/setup-terraform@v3
195+
with:
196+
terraform_version: ${{ steps.terraform-version.outputs.terraform_version }}
197+
198+
- name: "Change github.com url in modules to local directories and add backend prefix"
199+
if: ${{ steps.check-labels.outputs.is_ephemeral_staging == 'true' }}
200+
run: |
201+
cd activitypub-infra/infrastructure/activitypub-staging-environments
202+
sed -i 's/github\.com\/TryGhost/\.\.\/\.\.\/\.\./gI' main.tf
203+
sed -i 's/\?ref=main//g' main.tf
204+
sed -i 's/REPLACE_ME/${{ github.event.pull_request.number }}/g' terraform.tf
205+
206+
- name: "Authenticate with GCP"
207+
if: ${{ steps.check-labels.outputs.is_ephemeral_staging == 'true' }}
208+
uses: google-github-actions/auth@v2
209+
with:
210+
token_format: access_token
211+
workload_identity_provider: projects/687476608778/locations/global/workloadIdentityPools/github-oidc-activitypub/providers/github-provider-activitypub
212+
service_account: stg-activitypub-cicd-stg-envs@ghost-activitypub.iam.gserviceaccount.com
213+
214+
- name: "Terraform init"
215+
if: ${{ steps.check-labels.outputs.is_ephemeral_staging == 'true' }}
216+
run: |
217+
cd activitypub-infra/infrastructure/activitypub-staging-environments
218+
terraform init
219+
220+
- name: "Terraform apply"
221+
if: ${{ steps.check-labels.outputs.is_ephemeral_staging == 'true' }}
222+
run: |
223+
cd activitypub-infra/infrastructure/activitypub-staging-environments
224+
export TF_VAR_github_pr_number=${{ github.event.pull_request.number }}
225+
export TF_VAR_primary_region_name=netherlands
226+
export TF_VAR_migrations_image=europe-docker.pkg.dev/ghost-activitypub/activitypub/migrations:pr-${{ github.event.pull_request.number }}
227+
export TF_VAR_api_image=europe-docker.pkg.dev/ghost-activitypub/activitypub/activitypub:pr-${{ github.event.pull_request.number }}
228+
export TF_VAR_queue_image=europe-docker.pkg.dev/ghost-activitypub/activitypub/activitypub:pr-${{ github.event.pull_request.number }}
229+
terraform apply -auto-approve
230+
231+
- name: "Deploy Migrations to Cloud Run"
232+
if: ${{ steps.check-labels.outputs.is_ephemeral_staging == 'true' }}
233+
uses: google-github-actions/deploy-cloudrun@v2
234+
with:
235+
image: europe-docker.pkg.dev/ghost-activitypub/activitypub/migrations:pr-${{ github.event.pull_request.number }}
236+
region: europe-west4
237+
job: stg-pr-${{ github.event.pull_request.number }}-migrations
238+
flags: --wait --execute-now
239+
skip_default_labels: true
240+
labels: |-
241+
commit-sha=${{ github.sha }}
242+
243+
- name: "Update Load Balancer"
244+
if: ${{ steps.check-labels.outputs.is_ephemeral_staging == 'true' }}
245+
env:
246+
LABELS: ${{ toJson(github.event.pull_request.labels) }}
247+
GCP_PROJECT: ghost-activitypub
248+
run: |
249+
set -euo pipefail
250+
# Get current config
251+
gcloud compute url-maps export stg-activitypub --global --project ${GCP_PROJECT} > config.yml
252+
# Delete unnecessary fields
253+
yq 'del(.fingerprint)' config.yml -i
254+
yq 'del(.creationTimestamp)' config.yml -i
255+
export DEFAULT_SERVICE="https://www.googleapis.com/compute/v1/projects/ghost-activitypub/global/backendServices/stg-netherlands-activitypub-api"
256+
export PR_SERVICE="https://www.googleapis.com/compute/v1/projects/ghost-activitypub/global/backendServices/stg-pr-${{ github.event.pull_request.number }}-api"
257+
# Add host rules and path matchers if they don't exist
258+
yq '.hostRules = (.hostRules // [{"hosts": ["activitypub.infra.ghost.is"], "pathMatcher": "staging-environments"}])' config.yml > config.yml.tmp
259+
mv config.yml.tmp config.yml
260+
yq '.pathMatchers = (.pathMatchers // [{"name": "staging-environments", "defaultService": "'"$DEFAULT_SERVICE"'", "routeRules": []}])' config.yml > config.yml.tmp
261+
mv config.yml.tmp config.yml
262+
# Remove existing route rules for the PR service
263+
yq '.pathMatchers[] |= (.routeRules |= map(select((.routeAction.weightedBackendServices // []) | length == 0 or .routeAction.weightedBackendServices[0].backendService != env(PR_SERVICE))))' config.yml > config.yml.tmp
264+
mv config.yml.tmp config.yml
265+
# Add new route rules for the PR service
266+
export MAX_PRIORITY=$(yq '[.pathMatchers[] | select(.name == "staging-environments") | .routeRules[]?.priority] | max // 0' config.yml)
267+
export NEXT_PRIORITY=$((MAX_PRIORITY + 1))
268+
export HEADER_MATCHES=$(echo "$LABELS" | jq -c '[.[] | select(.name | test("\\.ghost\\.is$")) | { "headerName": "X-Forwarded-Host", "exactMatch": "\(.name)" }'])
269+
yq '.pathMatchers[0].routeRules += [{"priority": '"$NEXT_PRIORITY"', "matchRules": [{"prefixMatch": "/", "headerMatches": '$HEADER_MATCHES'}], "routeAction": {"weightedBackendServices": [ { "backendService": "'$PR_SERVICE'", "weight": 100 } ] } }]' config.yml > config.yml.tmp
270+
mv config.yml.tmp config.yml
271+
echo "Updating url map with:"
272+
cat config.yml
273+
gcloud compute url-maps import stg-activitypub --source=config.yml --global --project ${GCP_PROJECT} --quiet
274+
138275
deploy-staging:
139276
if: github.ref == 'refs/heads/main'
140277
name: (staging) Deploy
141-
environment: staging
142278
runs-on: ubuntu-latest
143279
needs: [build-test-push]
144280
strategy:
@@ -193,7 +329,6 @@ jobs:
193329
deploy-production:
194330
if: github.ref == 'refs/heads/main'
195331
name: (production) Deploy
196-
environment: production
197332
runs-on: ubuntu-latest
198333
needs: [build-test-push, deploy-staging]
199334
strategy:

0 commit comments

Comments
 (0)