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

chore: validate page object usage in new spec files on every PR #29915

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
5ea6f0f
base work for pom validation
seaona Jan 27, 2025
d11f3c4
yarn version
seaona Jan 27, 2025
0383d86
changed files as artifacts
seaona Jan 27, 2025
7c39992
wait for circle ci
seaona Jan 27, 2025
c209015
some debugging
seaona Jan 27, 2025
451fd5d
envars
seaona Jan 27, 2025
b450b65
fetch calls
seaona Jan 27, 2025
04c7dd4
rmv wait
seaona Jan 27, 2025
8a9f7c1
fix script
seaona Jan 27, 2025
ccabc5e
run
seaona Jan 27, 2025
62f8058
add env
seaona Jan 27, 2025
575b28d
error handling and wait time
seaona Jan 27, 2025
c8d27ab
fix url with envar
seaona Jan 28, 2025
8910bc8
wait
seaona Jan 28, 2025
725fab7
download buffer
seaona Jan 28, 2025
de4975b
directory checks
seaona Jan 28, 2025
da7acf5
remove tests and clean up
seaona Jan 28, 2025
9376bf7
Merge branch 'main' into page-object-enforce
seaona Jan 28, 2025
0b58c0c
on workflow call
seaona Jan 28, 2025
c501046
lint
seaona Jan 28, 2025
2adb681
reset envars again
seaona Jan 28, 2025
a65e2eb
env
seaona Jan 28, 2025
ba02294
smaller sleep
seaona Jan 28, 2025
28a43d2
review comment: update name
seaona Jan 28, 2025
d96ac91
review: address argument name and filter includes
seaona Jan 28, 2025
4702302
changedFilesPaths
seaona Jan 28, 2025
684366c
address more review comments
seaona Jan 29, 2025
3f58395
fix
seaona Jan 29, 2025
41427bd
back to js
seaona Jan 29, 2025
d49bd8b
Howard's fix .github/scripts/shared/circle-artifacts.ts
seaona Jan 30, 2025
9e15a13
Howard's suggestion getbranch
seaona Jan 30, 2025
0792a3b
add file status
seaona Feb 4, 2025
3bd0661
change workflow call for trigger
seaona Feb 4, 2025
0642693
test
seaona Feb 4, 2025
fdea90e
re-add tests as check has been successful
seaona Feb 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,9 @@ jobs:
root: .
paths:
- changed-files
- store_artifacts:
path: changed-files
destination: changed-files
Copy link
Contributor Author

Choose a reason for hiding this comment

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

before, we were just reading from the files, as the jobs that needed this were all in circle ci.
Now we need to store the results as an artifact, because we need to access the result from github actions too.


validate-locales-only:
executor: node-browsers-small
Expand Down
4 changes: 2 additions & 2 deletions .circleci/scripts/git-diff-default-branch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,13 @@ async function fetchUntilMergeBaseFound() {
* Performs a git diff command to get the list of files changed between the current branch and the origin.
* It first ensures that the necessary commits are fetched until the merge base is found.
*
* @returns The output of the git diff command, listing the changed files.
* @returns The output of the git diff command, listing the file paths with status (A, M, D).
* @throws If unable to get the diff after fetching the merge base or if an unexpected error occurs.
*/
async function gitDiff(): Promise<string> {
await fetchUntilMergeBaseFound();
const { stdout: diffResult } = await exec(
`git diff --name-only "origin/HEAD...${SOURCE_BRANCH}"`,
`git diff --name-status "origin/HEAD...${SOURCE_BRANCH}"`,
);
if (!diffResult) {
throw new Error('Unable to get diff after full checkout.');
Expand Down
6 changes: 4 additions & 2 deletions .circleci/scripts/test-run-e2e-timeout-minutes.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { fetchManifestFlagsFromPRAndGit } from '../../development/lib/get-manifest-flag';
import { filterE2eChangedFiles } from '../../test/e2e/changedFilesUtil';
import { filterE2eChangedFiles, readChangedAndNewFilesWithStatus, getChangedAndNewFiles } from '../../test/e2e/changedFilesUtil';

fetchManifestFlagsFromPRAndGit().then((manifestFlags) => {
let timeout;

if (manifestFlags.circleci?.timeoutMinutes) {
timeout = manifestFlags.circleci?.timeoutMinutes;
} else {
const changedOrNewTests = filterE2eChangedFiles();
const changedAndNewFilesWithStatus = readChangedAndNewFilesWithStatus();
const changedAndNewFiles = getChangedAndNewFiles(changedAndNewFilesWithStatus);
const changedOrNewTests = filterE2eChangedFiles(changedAndNewFiles);

// 20 minutes, plus 3 minutes for every changed file, up to a maximum of 30 minutes
timeout = Math.min(20 + changedOrNewTests.length * 3, 30);
Expand Down
5 changes: 3 additions & 2 deletions .circleci/scripts/validate-locales-only.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
const { readChangedFiles } = require('../../test/e2e/changedFilesUtil.js');
const { readChangedAndNewFilesWithStatus, getChangedAndNewFiles } = require('../../test/e2e/changedFilesUtil.js');

/**
* Verifies that all changed files are in the /_locales/ directory.
* Fails the build if any changed files are outside of the /_locales/ directory.
* Fails if no changed files are detected.
*/
function validateLocalesOnlyChangedFiles() {
const changedFiles = readChangedFiles();
const changedAndNewFilesWithStatus = readChangedAndNewFilesWithStatus();
const changedFiles = getChangedAndNewFiles(changedAndNewFilesWithStatus);
if (!changedFiles || changedFiles.length === 0) {
console.error('Failure: No changed files detected.');
process.exit(1);
Expand Down
38 changes: 9 additions & 29 deletions .devcontainer/download-builds.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { execSync } from 'child_process';
import util from 'util';

import {
getJobsByWorkflowId,
getPipelineId,
getWorkflowId,
} from '../.github/scripts/shared/circle-artifacts';
const exec = util.promisify(require('node:child_process').exec);

function getGitBranch() {
Expand All @@ -10,34 +14,10 @@ function getGitBranch() {
return gitOutput.match(branchRegex)?.groups?.branch || 'main';
}

async function getCircleJobs(branch: string) {
let response = await fetch(
`https://circleci.com/api/v2/project/gh/MetaMask/metamask-extension/pipeline?branch=${branch}`,
);

const pipelineId = (await response.json()).items[0].id;

console.log('pipelineId:', pipelineId);

response = await fetch(
`https://circleci.com/api/v2/pipeline/${pipelineId}/workflow`,
);

const workflowId = (await response.json()).items[0].id;

console.log('workflowId:', workflowId);

response = await fetch(
`https://circleci.com/api/v2/workflow/${workflowId}/job`,
);

const jobs = (await response.json()).items;

return jobs;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ℹ️ this logic is now in circle-artifacts file, as it's shared with the validate-page-object scriptl, as per @HowardBraham 's recommendation

async function getBuilds(branch: string, jobNames: string[]) {
const jobs = await getCircleJobs(branch);
const pipelineId = await getPipelineId(branch);
const workflowId = await getWorkflowId(pipelineId);
const jobs = await getJobsByWorkflowId(workflowId);
let builds = [] as any[];

for (const jobName of jobNames) {
Expand Down Expand Up @@ -137,7 +117,7 @@ function unzipBuilds(folder: 'builds' | 'builds-test', versionNumber: string) {
}

async function main(jobNames: string[]) {
const branch = getGitBranch();
const branch = process.env.CIRCLE_BRANCH || getGitBranch();

const builds = await getBuilds(branch, jobNames);

Expand Down
185 changes: 185 additions & 0 deletions .github/scripts/shared/circle-artifacts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import fs from 'fs';

// Set OWNER and REPOSITORY based on the environment
const OWNER =
process.env.CIRCLE_PROJECT_USERNAME || process.env.OWNER || 'MetaMask';
const REPOSITORY =
process.env.CIRCLE_PROJECT_REPONAME ||
process.env.REPOSITORY ||
'metamask-extension';
const CIRCLE_BASE_URL = 'https://circleci.com/api/v2';

/**
* Retrieves the pipeline ID for a given branch and optional commit hash.
*
* @param {string} branch - The branch name to fetch the pipeline for.
* @param {string} [headCommitHash] - Optional commit hash to match a specific pipeline.
* @returns {Promise<string>} A promise that resolves to the pipeline ID.
* @throws Will throw an error if no pipeline is found or if the HTTP request fails.
*/
export async function getPipelineId(branch: string, headCommitHash?: string): Promise<string> {
const url = `${CIRCLE_BASE_URL}/project/gh/${OWNER}/${REPOSITORY}/pipeline?branch=${branch}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch pipeline data: ${response.statusText}`);
}

const pipelineData = await response.json();
const pipelineItem = headCommitHash
? pipelineData.items.find((item: any) => item.vcs.revision === headCommitHash)
: pipelineData.items[0];
if (!pipelineItem) {
throw new Error('Pipeline ID not found');
}
console.log('pipelineId:', pipelineItem.id);

return pipelineItem.id;
}

/**
* Retrieves the workflow ID for a given pipeline ID.
*
* @param {string} pipelineId - The ID of the pipeline to fetch the workflow for.
* @returns {Promise<string>} A promise that resolves to the workflow ID.
* @throws Will throw an error if no workflow is found or if the HTTP request fails.
*/
export async function getWorkflowId(pipelineId: string): Promise<string> {
const url = `${CIRCLE_BASE_URL}/pipeline/${pipelineId}/workflow`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch workflow data: ${response.statusText}`);
}
const workflowData = await response.json();
const workflowId = workflowData.items[0]?.id;
if (!workflowId) {
throw new Error('Workflow ID not found');
}
console.log('workflowId:', workflowId);
return workflowId;
}

/**
* Retrieves a list of jobs for a given workflow ID.
*
* @param {string} workflowId - The ID of the workflow to fetch jobs for.
* @returns {Promise<any[]>} A promise that resolves to an array of jobs.
* @throws Will throw an error if no jobs are found or if the HTTP request fails.
*/
export async function getJobsByWorkflowId(workflowId: string): Promise<any[]> {
const url = `${CIRCLE_BASE_URL}/workflow/${workflowId}/job`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch jobs: ${response.statusText}`);
}
const jobs = (await response.json()).items;
return jobs;
}

/**
* Retrieves job details for a given workflow ID and optional job name.
*
* @param {string} workflowId - The ID of the workflow to fetch job details for.
* @param {string} [jobName] - Optional job name to match a specific job.
* @returns {Promise<any>} A promise that resolves to the job details.
* @throws Will throw an error if no job details are found or if the HTTP request fails.
*/
export async function getJobDetails(workflowId: string, jobName?: string): Promise<any> {
const jobs = await getJobsByWorkflowId(workflowId);
const jobDetails = jobName
? jobs.find((item: any) => item.name === jobName)
: jobs[0];
if (!jobDetails) {
throw new Error('Job details not found');
}
return jobDetails;
}

/**
* Retrieves the artifact URL for a given branch, commit hash, job name, and artifact name.
* @param {string} branch - The branch name.
* @param {string} headCommitHash - The commit hash of the branch.
* @param {string} jobName - The name of the job that produced the artifact.
* @param {string} artifactName - The name of the artifact to retrieve.
* @returns {Promise<string>} A promise that resolves to the artifact URL.
* @throws Will throw an error if the artifact is not found or if any HTTP request fails.
*/
export async function getArtifactUrl(branch: string, headCommitHash: string, jobName: string, artifactName: string): Promise<string> {
const pipelineId = await getPipelineId(branch, headCommitHash);
const workflowId = await getWorkflowId(pipelineId);
const jobDetails = await getJobDetails(workflowId, jobName);

const jobNumber = jobDetails.job_number;
console.log('Job number', jobNumber);

const artifactResponse = await fetch(`${CIRCLE_BASE_URL}/project/gh/${OWNER}/${REPOSITORY}/${jobNumber}/artifacts`);
const artifactData = await artifactResponse.json();
const artifact = artifactData.items.find((item: any) => item.path.includes(artifactName));

if (!artifact) {
throw new Error(`Artifact ${artifactName} not found`);
}

const artifactUrl = artifact.url;
console.log('Artifact URL:', artifactUrl);;

return artifactUrl;
}

/**
* Downloads an artifact from a given URL and saves it to the specified output path.
* @param {string} artifactUrl - The URL of the artifact to download.
* @param {string} outputFilePath - The path where the artifact should be saved.
* @returns {Promise<void>} A promise that resolves when the download is complete.
* @throws Will throw an error if the download fails or if the file cannot be written.
*/
export async function downloadArtifact(artifactUrl: string, outputFilePath: string): Promise<void> {
try {
// Ensure the directory exists
const dir = require('path').dirname(outputFilePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}

console.log(`Downloading artifact from URL: ${artifactUrl} to ${outputFilePath}`);

// Download the artifact
const artifactDownloadResponse = await fetch(artifactUrl);
if (!artifactDownloadResponse.ok) {
throw new Error(`Failed to download artifact: ${artifactDownloadResponse.statusText}`);
}
const artifactArrayBuffer = await artifactDownloadResponse.arrayBuffer();
const artifactBuffer = Buffer.from(artifactArrayBuffer);
fs.writeFileSync(outputFilePath, artifactBuffer);

if (!fs.existsSync(outputFilePath)) {
throw new Error(`Failed to download artifact to ${outputFilePath}`);
}

console.log(`Artifact downloaded successfully to ${outputFilePath}`);
} catch (error) {
console.error(`Error during artifact download: ${error.message}`);
throw error;
}
}

/**
* Reads the content of a file.
* @param filePath - The path to the file.
* @returns The content of the file.
*/
export function readFileContent(filePath: string): string {
if (!fs.existsSync(filePath)) {
throw new Error(`File not found: ${filePath}`);
}

const content = fs.readFileSync(filePath, 'utf-8').trim();
return content;
}

/**
* Sleep function to pause execution for a specified number of seconds.
* @param seconds - The number of seconds to sleep.
*/
export function sleep(seconds: number) {
return new Promise(resolve => setTimeout(resolve, seconds * 1000));
}
Loading
Loading