Skip to content

chore(ci): on demand review app #736

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions .github/workflows/review-app-comment-trigger.yml
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC, we'd now only deploy "ecospheres" by default, and all other sites would require /deploy. I'm tempted to have a list of defaults sites to deploy instead of just "ecospheres", but maybe not now...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be cool if new commits triggered redeploy for sites that were previously /deploy'ed. I don't see a simple, obvious way to do it, so probably later.

Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
name: Trigger Review App Deployment from Comments

on:
issue_comment:
types: [created]

jobs:
process_comment:
runs-on: ubuntu-latest
if: ${{ github.event.issue.pull_request && contains(github.event.comment.body, '/deploy') }}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if: ${{ github.event.issue.pull_request && contains(github.event.comment.body, '/deploy') }}
if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/deploy') }}

?


steps:
- name: Checkout code to access workflow file
uses: actions/checkout@v4

- name: Extract available sites
id: available_sites
uses: mikefarah/yq@de2f77b49cbd40fd67031ee602245d0acc4ac482
with:
cmd: yq '.on.workflow_dispatch.inputs.site.options[]' '.github/workflows/review-app.yml'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could go all yq and merge Extract+Format steps:

Suggested change
cmd: yq '.on.workflow_dispatch.inputs.site.options[]' '.github/workflows/review-app.yml'
cmd: yq '.on.workflow_dispatch.inputs.site.options | filter(. != "ecospheres") | join(",")' '.github/workflows/review-app.yml'


- name: Format available sites
id: format_sites
run: |
# Format the sites as a comma-separated list, excluding ecospheres
AVAILABLE_SITES=$(echo "${{ steps.available_sites.outputs.result }}" | grep -v "ecospheres" | tr '\n' ',' | sed 's/,$//')
echo "AVAILABLE_SITES=$AVAILABLE_SITES" >> $GITHUB_ENV
echo "available_sites=$AVAILABLE_SITES" >> $GITHUB_OUTPUT
echo "Available sites: $AVAILABLE_SITES"

- name: Get PR details
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you just compare github.event.pull_request.base.repo.full_name (target) and github.event.pull_request.head.repo.full_name (source)?

https://github.com/orgs/community/discussions/26829#discussioncomment-3253575

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be simpler to fail the workflow when !isSameRepo, so you don't have to test in every following step? Failure doesn't seem like bad behavior in that case.

id: pr_details
uses: actions/github-script@v6
with:
script: |
const prNumber = context.issue.number;
core.setOutput('pr_number', prNumber);

// Get PR information to verify it's from the same repository
const { data: pullRequest } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber
});

const isSameRepo = pullRequest.head.repo.full_name === context.repo.full_name;
core.setOutput('is_same_repo', isSameRepo.toString());

return { prNumber, isSameRepo };

- name: Parse deployment command
id: parse_command
if: ${{ steps.pr_details.outputs.is_same_repo == 'true' }}
run: |
COMMENT="${{ github.event.comment.body }}"
AVAILABLE_SITES="${{ env.AVAILABLE_SITES }}"

# Extract sites from command
if [[ $COMMENT =~ /deploy\ +([a-zA-Z0-9,-]+) ]]; then
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if [[ $COMMENT =~ /deploy\ +([a-zA-Z0-9,-]+) ]]; then
if [[ $COMMENT =~ /deploy\ +([a-zA-Z0-9,_-]+) ]]; then

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That comma got me wondering for a while. I'd move it up front and possibly add a comment? Or go with space-delimited site names?

SITES="${BASH_REMATCH[1]}"

# Remove spaces if any
SITES=$(echo $SITES | tr -d '[:space:]')

# If "all" is specified, use all available sites (excluding ecospheres)
if [[ "$SITES" == "all" ]]; then
SITES="$AVAILABLE_SITES"
fi

echo "SITES=$SITES" >> $GITHUB_ENV
echo "sites=$SITES" >> $GITHUB_OUTPUT
echo "has_valid_sites=true" >> $GITHUB_OUTPUT
else
# No site specified, fail
echo "No site specified in the deployment command" >> $GITHUB_STEP_SUMMARY
echo "has_valid_sites=false" >> $GITHUB_OUTPUT
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fail directly?

fi

- name: Add reaction to comment
if: ${{ steps.pr_details.outputs.is_same_repo == 'true' && steps.parse_command.outputs.has_valid_sites == 'true' }}
uses: actions/github-script@v6
with:
script: |
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: context.payload.comment.id,
content: 'rocket'
});

- name: Trigger deployments
if: ${{ steps.pr_details.outputs.is_same_repo == 'true' && steps.parse_command.outputs.has_valid_sites == 'true' }}
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const sites = '${{ steps.parse_command.outputs.sites }}'.split(',');
const prNumber = '${{ steps.pr_details.outputs.pr_number }}';
const availableSites = '${{ steps.format_sites.outputs.available_sites }}'.split(',').filter(Boolean);

// Create a comment to inform about the deployments
let deploymentMessage = `📦 Triggering deployment for site(s): **${sites.join(', ')}**\n\n`;
let validSitesFound = false;

for (const site of sites) {
// Skip ecospheres as it's deployed directly by PR events
if (site === 'ecospheres') {
deploymentMessage += `ℹ️ Note: Site 'ecospheres' is deployed automatically by PR events and will be skipped.\n`;
continue;
}

// Validate against available sites
if (!availableSites.includes(site)) {
deploymentMessage += `⚠️ Warning: Site '${site}' is not recognized and will be skipped.\n`;
continue;
}

validSitesFound = true;
deploymentMessage += `🚀 Starting deployment for **${site}**...\n`;

try {
await github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'review-app.yml',
ref: 'main',
inputs: {
site: site,
pr_number: prNumber
}
});
} catch (error) {
deploymentMessage += `❌ Failed to trigger deployment for ${site}: ${error.message}\n`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
deploymentMessage += `❌ Failed to trigger deployment for ${site}: ${error.message}\n`;
deploymentMessage += `❌ Failed to trigger deployment for '${site}': ${error.message}\n`;

continue;
}
}

if (!validSitesFound) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure that makes sense. If I /deploy a couple sites at once, I'd want to know about any invalid site, not just when all of them are.

deploymentMessage += `\n❌ No valid sites were found to deploy. Please check your command and try again.`;
}

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: deploymentMessage
});
94 changes: 63 additions & 31 deletions .github/workflows/review-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,56 +5,80 @@ name: Deploy review app
on:
pull_request:
types: [opened, synchronize, reopened, closed]
workflow_dispatch:
inputs:
site:
description: 'Site to deploy (ecospheres, meteo-france, or logistique)'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Description doesn't match options. I'd remove them to avoid syncing issues.

required: true
default: 'ecospheres'
type: choice
options:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sort for maintainability

- ecospheres
- meteo-france
- logistique
- hackathon
- defis
pr_number:
description: 'PR number (required for manual deployments)'
required: true
type: string

jobs:
handle_review_app:
runs-on: ubuntu-latest
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ matrix.site }}
group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.site || 'ecospheres' }}
cancel-in-progress: false
strategy:
matrix:
site:
- ecospheres
- meteo-france
- logistique
permissions:
deployments: write
# only run if the PR is from the same repo
if: github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request'
if: |
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
github.event_name == 'workflow_dispatch'
steps:
- name: Set environment variables
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "SITE_ID=${{ github.event.pull_request.head.ref == 'main' && 'ecospheres' || 'ecospheres' }}" >> $GITHUB_ENV
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't that always result in 'ecospheres'?

echo "PR_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_ENV
else
echo "SITE_ID=${{ inputs.site }}" >> $GITHUB_ENV
echo "PR_NUMBER=${{ inputs.pr_number }}" >> $GITHUB_ENV
fi

- name: Debug event
run: |
echo "Event name: ${{ github.event_name }}"
echo "Event action: ${{ github.event.action }}"
echo "Pull request state: ${{ github.event.pull_request.state }}"
echo "Pull request state: ${{ github.event.pull_request.state || 'N/A' }}"
echo "Site ID: ${{ env.SITE_ID }}"
echo "PR number: ${{ env.PR_NUMBER }}"

- name: Cloning repo
if: github.event.action != 'closed'
if: github.event_name == 'workflow_dispatch' || github.event.action != 'closed'
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Start deployment
if: github.event.action != 'closed'
if: github.event_name == 'workflow_dispatch' || github.event.action != 'closed'
uses: chrnorm/deployment-action@v2
id: deployment
with:
token: ${{ github.token }}
environment: ${{ matrix.site }}-preview
environment: ${{ env.SITE_ID }}-preview
initial-status: in_progress
transient-environment: true

- name: Create the review app
if: github.event.action == 'opened' || github.event.action == 'reopened'
if: github.event_name == 'workflow_dispatch' || github.event.action == 'opened' || github.event.action == 'reopened'
uses: dokku/github-action@master
# ignore errors as the app might already exist on workflow_dispatch
continue-on-error: true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be safer to split the two cases (workflow dispatch vs pr deploy) then?

with:
command: review-apps:create
git_remote_url: ${{ secrets.REVIEW_APP_SSH_URL }}
review_app_name: deploy-preview-${{ github.event.pull_request.number }}--${{ matrix.site }}
review_app_name: deploy-preview-${{ env.PR_NUMBER }}--${{ env.SITE_ID }}
ssh_private_key: ${{ secrets.REVIEW_APP_SSH_PRIVATE_KEY }}
# omitting this will prevent the app from being built (which is what we want)
# branch: 'main'

- name: Set site id as build arg
if: always()
Expand All @@ -65,43 +89,51 @@ jobs:
key: ${{ secrets.REVIEW_APP_SSH_PRIVATE_KEY }}
port: 22
script: |
docker-options:add deploy-preview-${{ github.event.pull_request.number }}--${{ matrix.site }} build "--build-arg VITE_SITE_ID=${{ matrix.site }}"
docker-options:add deploy-preview-${{ env.PR_NUMBER }}--${{ env.SITE_ID }} build "--build-arg VITE_SITE_ID=${{ env.SITE_ID }}"

- name: Push to dokku
if: github.event.action != 'closed'
if: github.event_name == 'workflow_dispatch' || github.event.action != 'closed'
uses: dokku/github-action@master
with:
git_remote_url: ${{ secrets.REVIEW_APP_SSH_URL }}
review_app_name: deploy-preview-${{ github.event.pull_request.number }}--${{ matrix.site }}
review_app_name: deploy-preview-${{ env.PR_NUMBER }}--${{ env.SITE_ID }}
ssh_private_key: ${{ secrets.REVIEW_APP_SSH_PRIVATE_KEY }}
git_push_flags: '--force'
branch: 'main'

- name: Enable SSL with Let's Encrypt
if: github.event.action == 'opened' || github.event.action == 'reopened'
if: github.event_name == 'workflow_dispatch' || github.event.action == 'opened' || github.event.action == 'reopened'
uses: appleboy/[email protected]
with:
host: ${{ secrets.REVIEW_APP_SSH_HOST }}
username: dokku
key: ${{ secrets.REVIEW_APP_SSH_PRIVATE_KEY }}
port: 22
script: |
letsencrypt:enable deploy-preview-${{ github.event.pull_request.number }}--${{ matrix.site }}
letsencrypt:enable deploy-preview-${{ env.PR_NUMBER }}--${{ env.SITE_ID }}

- name: Destroy the review app
if: github.event.action == 'closed'
uses: dokku/github-action@master
- name: Extract available sites
uses: mikefarah/yq@de2f77b49cbd40fd67031ee602245d0acc4ac482
id: get-sites
with:
command: review-apps:destroy
git_remote_url: ${{ secrets.REVIEW_APP_SSH_URL }}
review_app_name: deploy-preview-${{ github.event.pull_request.number }}--${{ matrix.site }}
ssh_private_key: ${{ secrets.REVIEW_APP_SSH_PRIVATE_KEY }}
cmd: yq '.on.workflow_dispatch.inputs.site.options[]' '.github/workflows/review-app.yml'

- name: Destroy all review apps
if: github.event.action == 'closed'
run: |
echo "${{ steps.get-sites.outputs.result }}" | while read site; do
echo "Destroying review app for site: $site"
ssh -i <(echo "${{ secrets.REVIEW_APP_SSH_PRIVATE_KEY }}") \
-o StrictHostKeyChecking=no \
dokku@${{ secrets.REVIEW_APP_SSH_HOST }} \
apps:destroy deploy-preview-${{ env.PR_NUMBER }}--$site || true
done

- name: Update deployment status
if: github.event.action != 'closed'
if: github.event_name == 'workflow_dispatch' || github.event.action != 'closed'
uses: chrnorm/deployment-status@v2
with:
token: ${{ github.token }}
environment-url: https://deploy-preview-${{ github.event.pull_request.number }}--${{ matrix.site }}.sandbox.data.developpement-durable.gouv.fr
environment-url: https://deploy-preview-${{ env.PR_NUMBER }}--${{ env.SITE_ID }}.sandbox.data.developpement-durable.gouv.fr
state: ${{ job.status == 'success' && 'success' || 'failure' }}
deployment-id: ${{ steps.deployment.outputs.deployment_id }}