Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ado-ext-telemetry): Embed upload artifact into task #1085

Merged
merged 12 commits into from
Mar 23, 2022
Merged
21 changes: 6 additions & 15 deletions docs/ado-extension-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,6 @@ steps:
# Provide either siteDir or url
# siteDir: '$(System.DefaultWorkingDirectory)/path-to-built-website/'
# url: 'your-website-url'

- publish: '$(System.DefaultWorkingDirectory)/_accessibility-reports'
displayName: Upload report artifact
condition: succeededOrFailed()
artifact: 'accessibility-reports'
```

### Scan a URL
Expand Down Expand Up @@ -136,30 +131,26 @@ Here is an example of a YAML file that is configured to take advantage of a base
inputs:
url: 'http://localhost:12345/'
baselineFile: '$(Build.SourcesDirectory)/baselines/my-web-site.baseline'

- publish: '$(System.DefaultWorkingDirectory)/_accessibility-reports'
displayName: Upload report artifacts
condition: succeededOrFailed()
artifact: 'accessibility-reports'
```

## Viewing results

- An HTML report containing detailed results is saved to disk. To view it, you need to have added the step to upload artifacts to your yml file (see [Basic template](#basic-template)). Navigate to Artifacts from the build. Under `accessibility-reports`, you'll find the downloadable report labeled `index.html`.

- If the workflow was triggered by a pull request, the action should leave a comment on the Azure DevOps pull request with results. The extension does not leave comments on repos in GitHub.
- Summary results along with a link to report artifacts are output in both the task log and in the Extensions tab of the pipeline run. To view the task log, click into the job and then click on the accessibilityinsights task.
brocktaylor7 marked this conversation as resolved.
Show resolved Hide resolved
- An HTML report containing detailed results is saved as a pipeline artifact. To view it, navigate to Artifacts from the build. Under `accessibility-reports`, you'll find the downloadable report labeled `index.html`.

## Blocking pull requests

You can choose to block pull requests if the extension finds accessibility issues.

1. Ensure the extension is [triggered on each pull request](https://docs.microsoft.com/en-us/azure/devops/pipelines/customize-pipeline?view=azure-devops#customize-ci-triggers).
2. Ensure that you have set the `failOnAccessibilityError` input variable to `true`.
3. Ensure that the `Upload report artifact` step runs even in failure cases using [**succeededOrFailed()**](https://docs.microsoft.com/en-us/azure/devops/pipelines/process/conditions?view=azure-devops&tabs=yaml)

## Running multiple times in a single pipeline

If you want to run the extension multiple times in a single pipeline, you will need to ensure that unique `artifactName` and `outputDir` inputs are specified in your YAML file for each task. Artifact names and the output directory must be unique across all tasks in a pipeline.

## Troubleshooting

- If the action didn't trigger as you expected, check the `trigger` or `pr` sections of your yml file. Make sure any listed branch names are correct for your repository.
- If the action fails to complete, you can check the build logs for execution errors. Using the template above, these logs will be in the `Scan for accessibility issues` step.
- If you can't find an artifact, note that your workflow must include a `publish` step to add the report folder to your check results. See the [Basic template](#basic-template) above and [Azure DevOps documentation on publishing artifacts](https://docs.microsoft.com/en-us/azure/devops/pipelines/artifacts/pipeline-artifacts?view=azure-devops&tabs=yaml#publish-artifacts).
- If the scan takes longer than 90 seconds, you can override the default timeout by providing a value for `scanTimeout` in milliseconds.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ describe(adoStdoutTransformer, () => {
input | expectedOutput | safeTextIdentifier
${'##vso[task.uploadsummary]'} | ${'##vso[task.uploadsummary]'} | ${'task.uploadsummary'}
${'##vso[task.logissue]abc'} | ${'##vso[task.logissue]abc'} | ${'task.logissue'}
${'##vso[artifact.upload]abc'} | ${'##vso[artifact.upload]abc'} | ${'artifact.upload'}
`(`ADO Special logging command '$safeTextIdentifier' returns as expected`, ({ input, expectedOutput }) => {
const output = adoStdoutTransformer(input);
expect(output).toBe(expectedOutput);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ const regexTransformations: RegexTransformation[] = [
regex: new RegExp('^##vso\\[task.logissue\\]'),
method: useUnmodifiedString,
},
{
regex: new RegExp('^##vso\\[artifact.upload\\]'),
method: useUnmodifiedString,
},
{
regex: new RegExp('^Processing page .*'),
method: useUnmodifiedString,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ describe(AdoConsoleCommentCreator, () => {
let fsMock: IMock<typeof fs>;
const reportOutDir = 'reportOutDir';
const fileName = `${reportOutDir}/results.md`;
const artifactName = 'accessibility-reports';

beforeEach(() => {
adoTaskConfigMock = Mock.ofType<ADOTaskConfig>(undefined, MockBehavior.Strict);
Expand Down Expand Up @@ -58,6 +59,72 @@ describe(AdoConsoleCommentCreator, () => {
adoTaskConfigMock
.setup((atcm) => atcm.getReportOutDir())
.returns(() => reportOutDir)
.verifiable(Times.exactly(2));

adoTaskConfigMock
.setup((atcm) => atcm.getVariable('System.JobAttempt'))
.returns(() => '1')
.verifiable(Times.once());

adoTaskConfigMock
.setup((atcm) => atcm.getArtifactName())
.returns(() => artifactName)
.verifiable(Times.once());

reportMarkdownConvertorMock
.setup((o) => o.convert(reportStub, undefined, baselineInfoStub))
.returns(() => expectedLogOutput)
.verifiable(Times.once());

reportConsoleLogConvertorMock
.setup((o) => o.convert(reportStub, undefined, baselineInfoStub))
.returns(() => expectedLogOutput)
.verifiable(Times.once());

loggerMock.setup((lm) => lm.logInfo(expectedLogOutput)).verifiable(Times.once());
loggerMock.setup((lm) => lm.logInfo(`##vso[task.uploadsummary]${fileName}`)).verifiable(Times.once());
loggerMock
.setup((lm) => lm.logInfo(`##vso[artifact.upload artifactname=${artifactName}]${reportOutDir}`))
.verifiable(Times.once());
fsMock.setup((fsm) => fsm.writeFileSync(fileName, expectedLogOutput)).verifiable();

adoConsoleCommentCreator = buildAdoConsoleCommentCreatorWithMocks();
await adoConsoleCommentCreator.completeRun(reportStub);

verifyAllMocks();
});

it('Successfully adds suffix to output name if job attmept > 1', async () => {
const reportStub: CombinedReportParameters = {
results: {
urlResults: {
failedUrls: 1,
},
},
} as CombinedReportParameters;
const baselineInfoStub = {};
const reportMarkdownStub = '#ReportMarkdownStub';

const expectedLogOutput = reportMarkdownStub;

adoTaskConfigMock
.setup((atcm) => atcm.getBaselineFile())
.returns(() => undefined)
.verifiable(Times.once());

adoTaskConfigMock
.setup((atcm) => atcm.getReportOutDir())
.returns(() => reportOutDir)
.verifiable(Times.exactly(2));

adoTaskConfigMock
.setup((atcm) => atcm.getVariable('System.JobAttempt'))
.returns(() => '2')
.verifiable(Times.once());

adoTaskConfigMock
.setup((atcm) => atcm.getArtifactName())
.returns(() => artifactName)
.verifiable(Times.once());

reportMarkdownConvertorMock
Expand All @@ -72,6 +139,9 @@ describe(AdoConsoleCommentCreator, () => {

loggerMock.setup((lm) => lm.logInfo(expectedLogOutput)).verifiable(Times.once());
loggerMock.setup((lm) => lm.logInfo(`##vso[task.uploadsummary]${fileName}`)).verifiable(Times.once());
loggerMock
.setup((lm) => lm.logInfo(`##vso[artifact.upload artifactname=${artifactName}-2]${reportOutDir}`))
.verifiable(Times.once());
fsMock.setup((fsm) => fsm.writeFileSync(fileName, expectedLogOutput)).verifiable();

adoConsoleCommentCreator = buildAdoConsoleCommentCreatorWithMocks();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export class AdoConsoleCommentCreator extends ProgressReporter {
public async completeRun(combinedReportResult: CombinedReportParameters, baselineEvaluation?: BaselineEvaluation): Promise<void> {
const baselineInfo = this.getBaselineInfo(baselineEvaluation);
this.outputResultsMarkdownToBuildSummary(combinedReportResult, baselineInfo);
this.uploadReportArtifacts();
this.logResultsToConsole(combinedReportResult, baselineInfo);

return Promise.resolve();
Expand Down Expand Up @@ -62,6 +63,22 @@ export class AdoConsoleCommentCreator extends ProgressReporter {
this.logger.logInfo(`##vso[task.uploadsummary]${fileName}`);
}

private uploadReportArtifacts(): void {
const outputDirectory = this.taskConfig.getReportOutDir();
const jobAttemptBuildVariable = this.taskConfig.getVariable('System.JobAttempt');
const artifactName = this.taskConfig.getArtifactName();

let artifactNameSuffix = '';
let jobAttemptNumber = 1;

if (jobAttemptBuildVariable !== undefined) {
jobAttemptNumber = parseInt(jobAttemptBuildVariable);
artifactNameSuffix = jobAttemptNumber > 1 ? `-${jobAttemptBuildVariable}` : '';
}

this.logger.logInfo(`##vso[artifact.upload artifactname=${artifactName + artifactNameSuffix}]${outputDirectory}`);
}

private logResultsToConsole(combinedReportResult: CombinedReportParameters, baselineInfo?: BaselineInfo): void {
const reportMarkdown = this.reportConsoleLogConvertor.convert(combinedReportResult, undefined, baselineInfo);

Expand Down
14 changes: 14 additions & 0 deletions packages/ado-extension/src/task-config/ado-task-config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ describe(ADOTaskConfig, () => {
${'baselineFile'} | ${'./baselineFile'} | ${getPlatformAgnosticPath(__dirname + '/baselineFile')} | ${() => taskConfig.getBaselineFile()}
${'failOnAccessibilityError'} | ${true} | ${true} | ${() => taskConfig.getFailOnAccessibilityError()}
${'singleWorker'} | ${true} | ${true} | ${() => taskConfig.getSingleWorker()}
${'artifactName'} | ${'artifact-name'} | ${'artifact-name'} | ${() => taskConfig.getArtifactName()}
`(
`input value '$inputValue' returned as '$expectedValue' for '$inputOption' parameter`,
({ inputOption, getInputFunc, inputValue, expectedValue }) => {
Expand Down Expand Up @@ -164,4 +165,17 @@ describe(ADOTaskConfig, () => {

expect(actualCommitHash).toEqual(commitHash);
});

it('should call get variable from task library', () => {
const variableName = 'variableName';
const variableValue = 'variableValue';
adoTaskMock
.setup((o) => o.getVariable(variableName))
.returns(() => variableValue)
.verifiable(Times.once());

const actualVariableValue = taskConfig.getVariable(variableName);

expect(actualVariableValue).toEqual(variableValue);
});
});
9 changes: 9 additions & 0 deletions packages/ado-extension/src/task-config/ado-task-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,15 @@ export class ADOTaskConfig extends TaskConfig {
return this.processObj.env.SYSTEM_TEAMPROJECT ?? undefined;
}

public getArtifactName(): string {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.adoTaskObj.getInput('artifactName')!;
}

public getVariable(definedVariableName: string): string | undefined {
return this.adoTaskObj.getVariable(definedVariableName);
}

private getAbsolutePath(path: string | undefined): string | undefined {
if (isEmpty(path)) {
return undefined;
Expand Down
8 changes: 8 additions & 0 deletions packages/ado-extension/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,14 @@
"required": true,
"defaultValue": true,
"helpMarkDown": "To get deterministic scanning results, either specify the singleWorker parameter or ensure that the value specified for the maxUrls parameter is larger than the total number of urls in the web site being scanned."
},
{
"name": "artifactName",
"type": "string",
"label": "Artifact Name",
"required": false,
"defaultValue": "accessibility-reports",
"helpMarkDown": "Name of the report artifact to be uploaded to the build."
brocktaylor7 marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: It might be good to be explicit about how this interacts with uploadResultsAsArtifact by adding a visibleRule for pipeline UX users and a Ignored if uploadResultAsArtifact is false. to the helpMarkdown for yaml users

}
],
"execution": {
Expand Down