Continuous Integration Secure #21109
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.event == 'pull_request' && | |
github.event.workflow_run.conclusion == 'success' && | |
github.event.workflow_run.pull_requests[0] != null | |
permissions: | |
deployments: write | |
packages: write | |
pull-requests: write | |
steps: | |
- uses: actions/checkout@v4 | |
- uses: actions/setup-node@v4 | |
with: | |
node-version-file: .nvmrc | |
cache: yarn | |
- 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 | |
uses: actions/github-script@v7 | |
with: | |
script: | | |
const environment = `pr${process.env.PR_NUMBER}`; | |
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`, | |
}); | |
- name: 'Container: Build and preview image' | |
run: | | |
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin | |
docker build --tag $IMAGE_REPO_PREVIEW:pr$PR_NUMBER . | |
docker push $IMAGE_REPO_PREVIEW:pr$PR_NUMBER | |
env: | |
DOCKER_BUILDKIT: 1 | |
- name: "Add 'preview-available' label" | |
# This label is used for filtering deployments in ArgoCD | |
run: gh issue edit $PR_NUMBER --add-label "preview-available" | |
env: | |
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
visual-regression: | |
runs-on: ubuntu-latest | |
if: > | |
github.event.workflow_run.event == 'pull_request' && | |
github.event.workflow_run.conclusion == 'success' && | |
github.event.workflow_run.pull_requests[0] != null | |
permissions: | |
checks: write | |
deployments: 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: 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' }), | |
}); | |
const environment = `pr${process.env.PR_NUMBER}-diff`; | |
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: success ? 'inactive' : 'in_progress', | |
environment_url: diffUrl, | |
}); | |
}; | |
// If we have no screenshots, we do not need to create a check. | |
if (readdirSync('dist/screenshots').length <= 1) { | |
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: Create visual regression container | |
if: steps.screenshot-check.outputs.result == 'changed' || steps.screenshot-check.outputs.result == 'empty' | |
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 | |
env: | |
DOCKER_BUILDKIT: 1 | |
- name: Apply labels | |
if: steps.screenshot-check.outputs.result == 'changed' || steps.screenshot-check.outputs.result == 'empty' | |
run: | | |
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 "diff-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' && | |
github.event.workflow_run.pull_requests[0] != null | |
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 |