I'm using BackstopJS for regression tests and trying to implement GitHub workflow.
At first little introduction how BackstopJS works:
We have reference images (pictures) of browser pages
We run BackstopJS test and compare actual browser view and reference image
Check backstop report HTML page in browser and decide which is correct actual view or reference image
If browser view is an updated correct version, we run backstop approve command to rewrite reference image with new actual image
What can be implemented inside GitHub actions:
Download reference images from S3 bucket
Run BackstopJS test
Save HTML report and actual browser images as artifacts
Download HTML report stored as artifact and check if new version of images is correct
??? Here is a problem
Problem:
Workflow is already ended, and we don't able to approve new images. So, is here any way to add dialog inside Pull Request if test Action failed to be able upload new images (stored as artifacts) to S3 as new reference images? Or some way to retry failed test with new parameters (let's say it will be env AUTO_APPROVE=true) to be able re-run test with new images approvement?
Finally, I implemented interactive workflow:
---
name: 'BackstopJS test'
on:
pull_request:
types:
- edited
- opened
- synchronize
branches:
- 'develop'
env:
AWS_ACCOUNT_ID: '12345678'
AWS_REGION: 'us-east-1'
AWS_BUCKET_NAME: 'bucket_name'
AWS_BUCKET_PATH: 'bucket_folder'
AWS_BUCKET_KEY: 'bitmaps_archive.zip'
defaults:
run:
shell: bash
working-directory: backstop_test
jobs:
test:
# yamllint disable rule:line-length
if: ${{ (github.event.action != 'edited' ) || contains(github.event.pull_request.body, 'approve ') }}
name: 'BackstopJS test'
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
pull-requests: read
steps:
- name: Checkout
uses: actions/checkout#v2
- name: Get last commit message (only 100 commits in PR are acceptable)
if: ${{ github.event.action != 'edited' }}
env:
COMMITS_URL: ${{ github.event.pull_request.commits_url }}
run: |
if [ "${COMMITS_URL}x" != "x" ]; then
echo "COMMIT_MSG=$(curl -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" "${COMMITS_URL}?per_page=100" | jq -r .[-1].commit.message)" >> "${GITHUB_ENV}"
else
echo '::warning ::Cannot get commits list URL'
echo 'COMMIT_MSG=' >> "${GITHUB_ENV}"
fi
- name: Search for approve directives in PR body or commit message
shell: python
env:
PR_MSG: ${{ github.event.pull_request.body }}
run: |
from os import environ as env
from sys import exit
file_path = env.get('GITHUB_ENV', None)
if file_path is None:
raise OSError('Environ file not found')
autoapprove = False
approve_only = False
commit_message = env.get('COMMIT_MSG', '')
pr_message = env.get('PR_MSG', '')
with open(file_path, 'a') as gh_envs:
if '[cancel test]' in commit_message.lower() or '[skip test]' in commit_message.lower():
gh_envs.write('SKIP_TEST=1\n')
print("::warning ::Test is skipped by commit tag")
exit(0)
elif 'cancel test' in pr_message.lower() or 'skip test' in pr_message.lower():
gh_envs.write('SKIP_TEST=1\n')
print("::warning ::Test is skipped by tag in PR message")
exit(0)
else:
gh_envs.write('SKIP_TEST=0\n')
if '[approve me]' in commit_message.lower():
autoapprove = True
approve_only = True
print("Reference bitmaps will be approved by commit message")
else:
print("Last commit message:", commit_message)
if '${{ github.event.action }}' == 'edited':
approve_only = True
pr_message = pr_message.split('\n')
last_commit_id = '${{ github.event.pull_request.head.sha }}'
commit_id = None
for line in pr_message:
if line.startswith('approve '):
commit_id = line.split(' ')[-1].rstrip('\n\r')
break
if commit_id:
if last_commit_id.startswith(commit_id):
autoapprove = True
else:
print(
"::warning ::approved commit sha and last commit sha are missmatched:",
commit_id,
"/",
last_commit_id
)
else:
print("Auto approvment disabled")
with open(file_path, 'a') as gh_envs:
if autoapprove:
gh_envs.write('AUTOAPPROVE=1\n')
else:
gh_envs.write('AUTOAPPROVE=0\n')
if approve_only and autoapprove:
gh_envs.write('APPROVE_ONLY=1\n')
else:
gh_envs.write('APPROVE_ONLY=0\n')
- name: Configure AWS credentials
if: ${{ env.SKIP_TEST != 1 }}
uses: aws-actions/configure-aws-credentials#v1
with:
role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/github-iam-role
aws-region: ${{ env.AWS_REGION }}
role-session-name: backstopjs_test_runner
- name: Download and extract reference bitmaps
if: ${{ env.SKIP_TEST != 1 }}
run: |
aws s3 cp "s3://${AWS_BUCKET_NAME}/${AWS_BUCKET_PATH}/${AWS_BUCKET_KEY}" "./backup_${AWS_BUCKET_KEY}" && unzip -od ./backstop_data "backup_${AWS_BUCKET_KEY}" || echo "::warning file=${AWS_BUCKET_KEY}::No reference bitmaps archive found"
- name: Run tests
if: ${{ env.SKIP_TEST != 1 }}
run: |
if [ "${AUTOAPPROVE}" == "1" ] || [ "${AUTOAPPROVE^^}" == "TRUE" ] || [ "${AUTOAPPROVE^^}" == "YES" ]; then
echo "**Autoapprove is activated. Reference images will be renewed** " >> "${GITHUB_STEP_SUMMARY}"
else
{
echo "**Autoapprove is not active. Current reference images will be used** ";
echo "";
echo "Add \`approve ${{ github.event.pull_request.head.sha }}\` line to PR description";
echo "and test JOB will be automatically re-run to approve new reference bitmaps";
echo "";
echo "* if you will do new commit into PR, \`sha\` of current approvment should be updated too ";
echo "";
echo "Also \`[approve me]\` tag may be added inside commit message to renew bitmaps automatically ";
} >> "${GITHUB_STEP_SUMMARY}"
fi
{
echo "";
echo "Test may be cancelled by using \`[skip test]\` (or \`[cancel test]\`) tag inside commit message ";
echo "or by using \`skip test\` (or \`cancel test\`) code word inside PR message ";
echo "";
echo "---";
} >> "${GITHUB_STEP_SUMMARY}"
# Run tests here.
# If AUTOAPPROVE=1 then backstop test → backstop approve → backstop test will be run (both reports will be saved)
# If AUTOAPPROVE=1 and APPROVE_ONLY=1 then backstop reference → backstop test will be run
# ...
# Set Job to fail or success depends on tests exit status code
# In this example tests are not included, and status will be always failed if autoapprove is 0
if [ "${AUTOAPPROVE}x" == "1x" ]; then
echo "IS_FAILED=0" >> "${GITHUB_ENV}"
else
echo "IS_FAILED=1" >> "${GITHUB_ENV}"
echo "Error: test \`BLAHBLAHBLAH\` failed with status code: \`1\`" >> "${GITHUB_STEP_SUMMARY}"
fi
- name: Upload new reference ritmaps to S3 bucket
if: ${{ env.AUTOAPPROVE == 1 && env.IS_FAILED == 0 && env.SKIP_TEST != 1 }}
run: |
cd backstop_data && zip -ur "../${AWS_BUCKET_KEY}" bitmaps_reference && cd .. && \
aws s3 cp "${AWS_BUCKET_KEY}" "s3://${AWS_BUCKET_NAME}/${AWS_BUCKET_PATH}/${AWS_BUCKET_KEY}"
if [ -f "backup_${AWS_BUCKET_KEY}" ]; then
aws s3 cp "backup_${AWS_BUCKET_KEY}" "s3://${AWS_BUCKET_NAME}/${AWS_BUCKET_PATH}/backup_${AWS_BUCKET_KEY}"
fi
- name: Save HTML reports
if: ${{ env.SKIP_TEST != 1 }}
uses: actions/upload-artifact#v3
with:
name: html_reports
path: backstop_test/report
- name: Save logs (only if failed)
if: ${{ env.IS_FAILED == 1 && env.SKIP_TEST != 1 }}
uses: actions/upload-artifact#v3
with:
name: test_logs
path: backstop_test/logs
- name: Set to fail
if: ${{ env.IS_FAILED == 1 && env.SKIP_TEST != 1 }}
uses: actions/github-script#v3
with:
script: |
core.setFailed('Some of regression tests failed. Check Summary for detailed info')
Flow runs on:
PR contents updates (updated)
New commits inside PR (synchronize)
New PR created (opened)
If PRs body has line skip test (cancel test) or commits message has tag [skip test] ([cancel test]), then test will be skipped
If PRs body has line approve commit-SHA (where commit-SHA is a sha of a last commit in PR) or commits message has tag [approve me], then new reference bitmaps will be created
If approve line is present in PR, only one test with new reference images will be run
If approve tag is present in commit message, two tests will be run (before and after approvement) and two reports will be saved
Reference images are uploaded/downloaded/stored from/to/in S3 bucket
Related
In Azure Devops, I can set a pipeline variable at runtime by echoing:
##vso[task.setVariable var=value]
How can I do the same thing in Github Workflows?
I'm not making a custom action, so I don't think outputs are relevant, I just want to pass a variable from one step to another. However, I might be missing something.
The following will set a value as an env variable named environment_variable_name
echo "{environment_variable_name}={value}" >> $GITHUB_ENV
An example on how you would use this could be
steps:
- name: Set the value
id: step_one
run: |
echo "action_state=yellow" >> $GITHUB_ENV
- name: Use the value
id: step_two
run: |
echo "${{ env.action_state }}" # This will output 'yellow'
More on this can be found here
I wanted to pass secrets from a GitHub action to a JSON file in the same workflow.
# Github secrets
SECRET_TOKEN: 4321
In file.json the SECRET_TOKEN value needs to be fetched.
# file.json
{
secret_token: "SECRET_TOKEN", # should fetch the SECRET_TOKEN from git action
apiId: "blabla"
}
Expected Output:
# file.json
{
secret_token: "4321",
apiId: "blabla"
}
You have several options - you can use pure bash and jq to achieve that or if you are not that experienced, an easier way will be to use one of existing actions from marketplace, like this one:
https://github.com/marketplace/actions/create-json
- name: create-json
uses: jsdaniell/create-json#1.1.2
with:
name: "file.json"
json: '{"app":"blabla", "secret_token":"${{ secrets.SECRET_TOKEN }}"}'
I would suggest you to use the replace-tokens action, as example, suppose this json file:
file.json
{
secret_token: "#{SECRET_TOKEN}#",
apiId: "blabla"
}
the action:
- uses: cschleiden/replace-tokens#v1
with:
files: 'file.json'
env:
SECRET_TOKEN: ${{ secrets.SECRET_TOKEN }}
If you want to use a different token format, you can specify a custom token prefix/suffix. For example, to replace just tokens like `{SECRET_TOKEN} you could add:
- uses: cschleiden/replace-tokens#v1
with:
files: 'file.json'
tokenPrefix: '{'
tokenSuffix: '}'
env:
SECRET_TOKEN: ${{ secrets.SECRET_TOKEN }}
I have a workflow that is triggered by workflow_dispatch events with a few non-required string inputs and am trying to figure out how to determine if the value was provided or not.
on:
workflow_dispatch:
inputs:
input1:
description: first input
required: false
type: string
input2:
description: second input
required: false
type: string
The documentation says that unset inputs that are of type string will be equated to an empty string in the workflow but when I check that in an if clause condition for a job, it doesn't seem to be evaluating properly.
jobs:
jobA:
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.input1 != '' }}
# ...
Even when I dispatch the workflow with the input empty, both steps are ran.
What is the idiomatic way of checking if an input value was unset or not if this is not it?
You don't need the ${{ }} in this case, just using:
if: github.event_name == 'workflow_dispatch' && github.event.inputs.input1 != ''
Will work
I made an example here if you want to have a look:
workflow file
workflow run (input1 NOT empty)
workflow run (input1 IS empty)
I want to convert a pipeline variable - delimited string - to a json array and assign the json array to an other pipeline variable. See my code below, output stays empty. What am I missing here?
script:
steps:
- task: PowerShell#2
inputs:
targetType: inline
script: |
$test = "LZ-UK;LZ-ES;LZ-NL"
$json = $test.Split(";") | ConvertTo-Json -AsArray
Write-Host "##vso[task.setvariable variable=JsonLZ]$json"
Write-Host "Assigned the variable"
Write-Host "New `r`n $($JsonLZ)"
- script: |
echo ${{ variables.JsonLZ }}
output:
Starting: PowerShell
==============================================================================
Task : PowerShell
Description : Run a PowerShell script on Linux, macOS, or Windows
Version : 2.200.0
Author : Microsoft Corporation
Help : https://learn.microsoft.com/azure/devops/pipelines/tasks/utility/powershell
==============================================================================
Generating script.
========================== Starting Command Output ===========================
/usr/bin/pwsh -NoLogo -NoProfile -NonInteractive -Command . '/home/vsts/work/_temp/380b437f-74c4-4883-9d4a-7b4f3ac79266.ps1'
"LZ-UK",
"LZ-ES",
"LZ-NL"
]
Assigned the variable
New
Finishing: PowerShell
You're very close. There were a few minor issues that I spotted with your YAML/PowerShell:
You forgot the semicolon after the variable name in "##vso[task.setvariable variable=JsonLZ]$json", it should be: "##vso[task.setvariable variable=JsonLZ;]$json"
You should be using $(JsonLZ) instead of ${{ variables.JsonLZ }}. The former will be evaluated at runtime, the latter at compile-time. Here's a link to the MS Docs: Understand variable syntax
Give this a try to see a working example:
name: Stackoverflow-Example-Pipeline
trigger:
- none
variables:
JsonLZ: 'UNSET'
stages:
- stage: StageA
displayName: "Stage A"
jobs:
- job: example_job
displayName: "Example Job"
pool:
vmImage: "ubuntu-latest"
steps:
- task: PowerShell#2
inputs:
targetType: inline
script: |
$test = "LZ-UK;LZ-ES;LZ-NL"
$json = $test.Split(";") | ConvertTo-Json -Compress
Write-Host "##vso[task.setvariable variable=JsonLZ;]$json"
Write-Host "Assigned the variable"
- script: |
echo $(JsonLZ)
echo ${{ variables.JsonLZ }}
I'm writing a JavaScript action for GitHub Actions that has inputs, some of which are required. A simple example:
name: 'My action'
description: 'My description'
author: 'me'
inputs:
thisOneIsOptional:
description: 'An optional input'
required: false
thisOneIsRequired:
description: 'A required input'
required: true
runs:
using: 'node12'
main: '../lib/main.js'
What I find surprising is that I can use this action in a workflow without providing the required parameter and GitHub does not complain. It seems as though it is up to the action itself to validate that the required inputs were in fact provided. Is that right?
Is there anyway to get GitHub to validate this for me before my action code gets called?
Currently GitHub does not check if required input has been passed. This is being tracked in this issue.
However, you can implement validation yourself using bash, e.g.
- run: |
[[ "${{ inputs.docker_image_name }}" ]] || { echo "docker_image_name input is empty" ; exit 1; }
[[ "${{ inputs.doppler_token }}" ]] || { echo "doppler_token input is empty" ; exit 1; }
shell: bash