Skip to content

Continuous Integration Secure #24521

Continuous Integration Secure

Continuous Integration Secure #24521

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: Build slim image
uses: kitabisa/docker-slim-action@v1
with:
target: '${{ env.IMAGE_REPO_PREVIEW }}:pr${{ env.PR_NUMBER }}'
tag: 'pr${{ env.PR_NUMBER }}-slim'
env:
DSLIM_PRESERVE_PATH: /usr/share/nginx/html
- name: Push slim image
run: |
docker push $IMAGE_REPO_PREVIEW:pr$PR_NUMBER-slim
docker image list
- 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: Build slim image
uses: kitabisa/docker-slim-action@v1
with:
target: '${{ env.IMAGE_REPO_VISUAL_REGRESSION }}:pr${{ env.PR_NUMBER }}'
tag: 'pr${{ env.PR_NUMBER }}-slim'
env:
DSLIM_PRESERVE_PATH: /usr/share/nginx/html
- name: Push slim image
run: |
docker push $IMAGE_REPO_VISUAL_REGRESSION:pr$PR_NUMBER-slim
docker image list
- 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