Continuous Integration Secure #19103
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Continuous Integration Secure | |
# Secure execution of continuous integration jobs | |
# which are performed upon completion of the | |
# "Continuous Integration" workflow | |
# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ | |
on: | |
workflow_run: | |
workflows: [Continuous Integration] | |
types: [completed] | |
env: | |
IMAGE_REPO_PREVIEW: ghcr.io/${{ github.repository }}/storybook-preview | |
IMAGE_REPO_VISUAL_REGRESSION: ghcr.io/${{ github.repository }}/visual-regression | |
PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0] != null && github.event.workflow_run.pull_requests[0].number || '' }} | |
VISUAL_REQUIRED: 'pr: visual review required' | |
VISUAL_APPROVED: 'pr: visual review approved' | |
DIFF_URL: https://lyne-components-visual-regression-diff-pr{}.app.sbb.ch | |
jobs: | |
preview-image: | |
runs-on: ubuntu-latest | |
if: > | |
github.event.workflow_run.conclusion == 'success' && ( | |
github.event.workflow_run.event == 'pull_request' || ( | |
github.event.workflow_run.event == 'push' && | |
github.event.workflow_run.head_branch == 'main' | |
) | |
) | |
permissions: | |
deployments: write | |
packages: write | |
pull-requests: write | |
steps: | |
- uses: actions/checkout@v4 | |
- uses: actions/download-artifact@v4 | |
with: | |
name: storybook | |
path: dist/storybook/ | |
run-id: ${{ github.event.workflow_run.id }} | |
github-token: ${{ secrets.GH_ACTIONS_ARTIFACT_DOWNLOAD }} | |
- name: Remove files with forbidden extensions | |
run: node ./scripts/clean-storybook-files.cjs | |
- name: Create GitHub Deployment | |
id: tag-name | |
uses: actions/github-script@v7 | |
with: | |
script: | | |
const environment = process.env.PR_NUMBER ? `pr${process.env.PR_NUMBER}` : 'main'; | |
const payload = { owner: context.repo.owner, repo: context.repo.repo, environment }; | |
const { data: deployment } = await github.rest.repos.createDeployment({ | |
...payload, | |
ref: context.payload.workflow_run.head_sha, | |
auto_merge: false, | |
required_contexts: ['integrity', 'build', 'test', 'lint'] | |
}); | |
await github.rest.repos.createDeploymentStatus({ | |
...payload, | |
deployment_id: deployment.id, | |
state: 'in_progress', | |
environment_url: `https://${context.repo.repo}-${environment}.app.sbb.ch`, | |
}); | |
return environment; | |
result-encoding: string | |
- name: 'Container: Build and preview image' | |
run: | | |
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin | |
docker build --tag $IMAGE_REPO_PREVIEW:${{ steps.tag-name.outputs.result }} . | |
docker push $IMAGE_REPO_PREVIEW:${{ steps.tag-name.outputs.result }} | |
env: | |
DOCKER_BUILDKIT: 1 | |
- name: "Add 'preview-available' label" | |
if: env.PR_NUMBER != '' | |
# This label is used for filtering deployments in ArgoCD | |
run: gh issue edit ${{ env.PR_NUMBER }} --repo ${{ github.repository }} --add-label "preview-available" | |
env: | |
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
codecov: | |
runs-on: ubuntu-latest | |
if: > | |
github.event.workflow_run.event == 'pull_request' && | |
github.event.workflow_run.conclusion == 'success' | |
permissions: | |
pull-requests: write | |
steps: | |
- uses: actions/checkout@v4 | |
- uses: actions/download-artifact@v4 | |
with: | |
name: coverage | |
path: coverage/ | |
run-id: ${{ github.event.workflow_run.id }} | |
github-token: ${{ secrets.GH_ACTIONS_ARTIFACT_DOWNLOAD }} | |
- uses: codecov/codecov-action@v3 | |
with: | |
token: ${{ secrets.CODECOV_TOKEN }} | |
directory: coverage | |
override_branch: ${{ github.event.workflow_run.head_branch }} | |
override_commit: ${{ github.event.workflow_run.head_commit.id }} | |
override_pr: ${{ env.PR_NUMBER }} | |
fail_ci_if_error: true | |
verbose: true | |
visual-regression: | |
runs-on: ubuntu-latest | |
if: > | |
github.event.workflow_run.event == 'pull_request' && | |
github.event.workflow_run.conclusion == 'success' | |
permissions: | |
checks: write | |
packages: write | |
pull-requests: write | |
steps: | |
- uses: actions/checkout@v4 | |
- uses: actions/setup-node@v4 | |
with: | |
node-version-file: .nvmrc | |
cache: yarn | |
- run: yarn install --frozen-lockfile --non-interactive | |
- uses: actions/download-artifact@v4 | |
with: | |
name: visual-regression-screenshots | |
path: dist/screenshots/ | |
run-id: ${{ github.event.workflow_run.id }} | |
github-token: ${{ secrets.GH_ACTIONS_ARTIFACT_DOWNLOAD }} | |
- name: Remove .keep file | |
run: rm dist/screenshots/.keep | |
- name: Build visual-regression-app | |
run: yarn build:visual-regression-app | |
- name: Create check if changed | |
uses: actions/github-script@v7 | |
id: screenshot-check | |
with: | |
script: | | |
const { readdirSync, readFileSync } = await import('fs'); | |
const diffUrl = process.env.DIFF_URL.replace('{}', process.env.PR_NUMBER); | |
const diffInfo = JSON.parse(readFileSync('dist/visual-regression-app/diff.json', 'utf8')); | |
const createCheck = async (success) => await github.rest.checks.create({ | |
owner: context.repo.owner, | |
repo: context.repo.repo, | |
name: 'Visual Regression', | |
head_sha: context.payload.workflow_run.head_sha, | |
details_url: diffUrl, | |
output: { | |
title: `Visual Regression ${success ? 'Success' : `Failed (${diffInfo.changedAmount + diffInfo.newAmount})`}`, | |
summary: diffUrl, | |
text: `Changes: ${diffInfo.changedAmount}\nNew: ${diffInfo.newAmount}`, | |
}, | |
...(success ? { status: 'completed', conclusion: 'success' } : { status: 'in_progress' }), | |
}); | |
// If we have no screenshots, we do not need to create a check or containers. | |
if (!readdirSync('dist/screenshots').length) { | |
await createCheck(true); | |
return 'empty'; | |
} | |
let previousDiffInfo = {}; | |
try { | |
const response = await fetch(diffUrl + 'diff.json'); | |
if (response.ok) { | |
previousDiffInfo = await response.json(); | |
} | |
} catch {} | |
await createCheck(false); | |
// If the diff hash is the same as previously, we do not need to update the state or containers. | |
return diffInfo.hash === previousDiffInfo.hash ? 'no-change' : 'changed'; | |
result-encoding: string | |
- name: Remove labels when no failed screenshots exist | |
if: steps.screenshot-check.outputs.result == 'empty' | |
run: gh issue edit $PR_NUMBER --remove-label "$VISUAL_REQUIRED" | |
env: | |
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
- name: Add label and create container | |
if: steps.screenshot-check.outputs.result == 'changed' | |
run: | | |
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin | |
docker build \ | |
--file tools/visual-regression-testing/Dockerfile \ | |
--tag $IMAGE_REPO_VISUAL_REGRESSION:pr$PR_NUMBER \ | |
. | |
docker push $IMAGE_REPO_VISUAL_REGRESSION:pr$PR_NUMBER | |
gh issue edit $PR_NUMBER --remove-label "$VISUAL_APPROVED" | |
gh issue edit $PR_NUMBER --add-label "$VISUAL_REQUIRED" | |
gh issue edit $PR_NUMBER --add-label "visual-regression-diff-available" | |
env: | |
DOCKER_BUILDKIT: 1 | |
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |