Skip to content

Visual Regression Test (on GitHub-hosted) #48

Visual Regression Test (on GitHub-hosted)

Visual Regression Test (on GitHub-hosted) #48

Workflow file for this run

name: vrt
run-name: Visual Regression Test ${{ (vars.RUNS_ON_SELF_HOSTED == null && '(on GitHub-hosted)') || '(on self-hosted)' }}
on:
pull_request:
paths:
- 'lib/**'
- 'test/**'
- 'android/**'
- 'ios/**'
- 'pubspec.yaml'
- 'pubspec.lock'
- '.github/workflows/vrt.yml' # this file
workflow_dispatch:
permissions:
contents: read
pull-requests: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lookup:
name: lookup
runs-on: ${{ (vars.RUNS_ON_SELF_HOSTED == null && 'ubuntu-latest') || 'self-hosted' }}
timeout-minutes: 30
outputs:
actual-ref: ${{ steps.sha.outputs.actual-ref }}
actual-cache-hit: ${{ steps.actual-cache.outputs.cache-hit }}
expected-ref: ${{ steps.sha.outputs.expected-ref }}
expected-cache-hit: ${{ steps.expected-cache.outputs.cache-hit }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: ref for actual and expected
id: sha
shell: bash # set pipefail option
run: |
echo "actual-ref=${{ github.event.pull_request.merge_commit_sha }}" >> $GITHUB_OUTPUT
echo "expected-ref=$(git merge-base origin/$GITHUB_BASE_REF origin/$GITHUB_HEAD_REF)" >> $GITHUB_OUTPUT
- name: lookup actual cache
id: actual-cache
uses: actions/cache@v3
with:
key: reg-suit-cache-${{ steps.sha.outputs.actual-ref }}
path: .reg/actual
lookup-only: true
- name: lookup expected cache
id: expected-cache
uses: actions/cache@v3
with:
key: reg-suit-cache-${{ steps.sha.outputs.expected-ref }}
path: .reg/expected
lookup-only: true
crate-expected-images:
name: crate expected images
needs: lookup
if: ${{ !cancelled() && !failure() && needs.lookup.outputs.expected-cache-hit != 'true' }}
runs-on: ${{ (vars.RUNS_ON_SELF_HOSTED == null && 'ubuntu-latest') || 'self-hosted' }}
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.lookup.outputs.expected-ref }}
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ (vars.FLUTTER_VERSION != null && vars.FLUTTER_VERSION) || '' }}
channel: stable
cache: ${{ (vars.RUNS_ON_SELF_HOSTED == null && true) || false }}
- run: flutter pub get
- name: Run pre-process script
env:
PREPROCESS_SCRIPT_BASE64: ${{ secrets.PREPROCESS_SCRIPT_BASE64 }}
run: |
if [ -n "$PREPROCESS_SCRIPT_BASE64" ]; then
echo $PREPROCESS_SCRIPT_BASE64 | base64 --decode > ${{ runner.temp }}/pre-process.sh && bash ${{ runner.temp }}/pre-process.sh
fi
- name: Create golden images
run: |
flutter test --update-goldens --tags=golden
mkdir -p .reg/
mv test/golden_test/goldens .reg/expected
- uses: actions/cache/save@v3
with:
path: .reg/expected
key: reg-suit-cache-${{ needs.lookup.outputs.expected-ref }}
# - name: Upload articaft
# uses: actions/upload-artifact@v3
# with:
# name: reg-expected-artifact
# path: .reg/expected
crate-actual-images:
name: crate actual images
needs: lookup
if: ${{ !cancelled() && !failure() && needs.lookup.outputs.actual-cache-hit != 'true' }}
runs-on: ${{ (vars.RUNS_ON_SELF_HOSTED == null && 'ubuntu-latest') || 'self-hosted' }}
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.lookup.outputs.actual-ref }}
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ (vars.FLUTTER_VERSION != null && vars.FLUTTER_VERSION) || '' }}
channel: stable
cache: ${{ (vars.RUNS_ON_SELF_HOSTED == null && true) || false }}
- run: flutter pub get
- name: Run pre-process script
env:
PREPROCESS_SCRIPT_BASE64: ${{ secrets.PREPROCESS_SCRIPT_BASE64 }}
run: |
if [ -n "$PREPROCESS_SCRIPT_BASE64" ]; then
echo $PREPROCESS_SCRIPT_BASE64 | base64 --decode > ${{ runner.temp }}/pre-process.sh && bash ${{ runner.temp }}/pre-process.sh
fi
- name: Create golden images
run: |
flutter test --update-goldens --tags=golden
mkdir -p .reg/
mv test/golden_test/goldens .reg/actual
- uses: actions/cache/save@v3
with:
path: .reg/actual
key: reg-suit-cache-${{ needs.lookup.outputs.actual-ref }}
# - name: Upload articaft
# uses: actions/upload-artifact@v3
# with:
# name: reg-actual-artifact
# path: .reg/actual
compare:
name: compare
needs:
- lookup
- crate-actual-images
- crate-expected-images
if: ${{ !cancelled() && !failure() }}
runs-on: ${{ (vars.RUNS_ON_SELF_HOSTED == null && 'ubuntu-latest') || 'self-hosted' }}
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- name: restore yarn cache
uses: actions/cache@v3
with:
key: reg-suit-cache-yarn-${{ hashFiles('**/yarn.lock') }}
path: .yarn/cache/
- run: yarn install --immutable
- name: restore actual cache
uses: actions/cache/restore@v3
with:
key: reg-suit-cache-${{ needs.lookup.outputs.actual-ref }}
path: .reg/actual
- name: restore expected cache
uses: actions/cache/restore@v3
with:
key: reg-suit-cache-${{ needs.lookup.outputs.expected-ref }}
path: .reg/expected
- name: Compare by reg-suit
run: npx reg-suit compare 2>&1 | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' | tee reg-suit.log
- name: Upload .reg/ to articaft
uses: actions/upload-artifact@v3
with:
name: reg-suit-compare-result
path: .reg/
retention-days: 3
- name: Generate Comment
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const log = fs.readFileSync('./reg-suit.log', 'utf-8');
const parseStats = () => {
const lines = log.split('\n');
const stats = {
changed: 0,
newItems: 0,
deleted: 0,
passing: 0
};
for (const line of lines) {
if (line.includes('Changed items')) {
stats.changed = String(parseInt(line.split(': ')[1], 10));
} else if (line.includes('New items')) {
stats.newItems = String(parseInt(line.split(': ')[1], 10));
} else if (line.includes('Deleted items')) {
stats.deleted = String(parseInt(line.split(': ')[1], 10));
} else if (line.includes('Passed items')) {
stats.passing = String(parseInt(line.split(': ')[1], 10));
}
}
return stats;
};
const stats = parseStats();
const parseIcon = () => {
if (stats.changed != 0 || stats.deleted != 0) {
return '❗';
} else if (stats.newItems != 0) {
return '💡';
} else {
return '✅';
}
};
const icon = parseIcon();
const summaryCommentAnnotation = '<!-- Flutter VRT Test Summary Comment -->';
const markdown = await core.summary
.addHeading(`${icon} reg-suit has checked for visual changes`, 3)
.addTable([
["🔴 Changed", "🟡 New", "🟤 Deleted", "🔵 Passing"],
[stats.changed, stats.newItems, stats.deleted, stats.passing]
])
.addHeading("📊 Download Report", 3)
.addLink('You can download the report from the artifact here', `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`)
.addHeading("📝 Log", 3)
.addCodeBlock(log, "log")
.addRaw('<!-- Flutter VRT Test Summary Comment -->')
.stringify()
await core.summary.write()
const requestPerPage = 100;
try {
const listComments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
per_page: requestPerPage
});
const summaryComment = listComments.data.find(comment => comment.body.includes(summaryCommentAnnotation));
if (summaryComment) {
// delete summary comment if exists
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: summaryComment.id
});
}
// create a summary comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: markdown
});
} catch (error) {
logError(`Failed to modify comment: ${error.message}`);
return;
}
action-timeline:
needs: [lookup, crate-expected-images, crate-actual-images, compare]
runs-on: ubuntu-latest
steps:
- uses: Kesin11/actions-timeline@v2