From 5139166bdba86908fa1eee7c22ae5b9194dfc0d1 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Fri, 16 Aug 2024 17:14:14 -0700 Subject: [PATCH 01/32] Migrate contract changeset handling to solidity-jira --- .github/workflows/changeset.yml | 25 --------- .github/workflows/solidity-jira.yml | 83 +++++++++++++++-------------- 2 files changed, 42 insertions(+), 66 deletions(-) diff --git a/.github/workflows/changeset.yml b/.github/workflows/changeset.yml index 5e16b90c400..a89e91171e6 100644 --- a/.github/workflows/changeset.yml +++ b/.github/workflows/changeset.yml @@ -50,13 +50,8 @@ jobs: - '!core/**/*.json' - '!core/chainlink.goreleaser.Dockerfile' - '!core/chainlink.Dockerfile' - contracts: - - contracts/**/*.sol - - '!contracts/**/*.t.sol' core-changeset: - added: '.changeset/**' - contracts-changeset: - - added: 'contracts/.changeset/**' - name: Check for changeset tags for core id: changeset-tags @@ -120,19 +115,6 @@ jobs: mode: ${{ steps.files-changed.outputs.core-changeset == 'false' && 'upsert' || 'delete' }} create_if_not_exists: ${{ steps.files-changed.outputs.core-changeset == 'false' && 'true' || 'false' }} - - name: Make a comment - uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0 - if: ${{ steps.files-changed.outputs.contracts == 'true' }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - message: | - I see you updated files related to `contracts`. Please run `pnpm changeset` in the `contracts` directory to add a changeset. - reactions: eyes - comment_tag: changeset-contracts - mode: ${{ steps.files-changed.outputs.contracts-changeset == 'false' && 'upsert' || 'delete' }} - create_if_not_exists: ${{ steps.files-changed.outputs.contracts-changeset == 'false' && 'true' || 'false' }} - - name: Check for new changeset for core if: ${{ (steps.files-changed.outputs.core == 'true' || steps.files-changed.outputs.shared == 'true') && steps.files-changed.outputs.core-changeset == 'false' }} shell: bash @@ -140,13 +122,6 @@ jobs: echo "Please run pnpm changeset to add a changeset for core and include in the text at least one tag." exit 1 - - name: Check for new changeset for contracts - if: ${{ steps.files-changed.outputs.contracts == 'true' && steps.files-changed.outputs.contracts-changeset == 'false' }} - shell: bash - run: | - echo "Please run pnpm changeset to add a changeset for contracts." - exit 1 - - name: Make a comment uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0 if: ${{ steps.files-changed.outputs.core-changeset == 'true' }} diff --git a/.github/workflows/solidity-jira.yml b/.github/workflows/solidity-jira.yml index 1054bfa9875..f15e05884e9 100644 --- a/.github/workflows/solidity-jira.yml +++ b/.github/workflows/solidity-jira.yml @@ -1,11 +1,5 @@ -# This is its own independent workflow since "solidity.yml" depends on "merge_group" and "push" events. -# But for ensuring that JIRA tickets are always updated, we only care about "pull_request" events. -# -# We still need to add "merge_group" event and noop so that we'll pass required workflow checks. -# -# I didn't add this to the "changeset.yml" workflow because the "changeset" job isnt required, and we'd need to add the "merge_group" event to the "changeset.yml" workflow. -# If we made the change to make it required. -name: Solidity Jira +# This workflow handles the enforcement of code Traceability via changesets and jira issue linking for our Solidity codebase. +name: Solidity Tracability on: merge_group: @@ -16,58 +10,65 @@ defaults: shell: bash jobs: - skip-enforce-jira-issue: - name: Should Skip - # We want to skip merge_group events, and any release branches + files-changed: + # The job skips on merge_group events, and any release branches, and forks # Since we only want to enforce Jira issues on pull requests related to feature branches - if: ${{ github.event_name != 'merge_group' && !startsWith(github.head_ref, 'release/') }} - outputs: - should-enforce: ${{ steps.changed_files.outputs.only_src_contracts }} + if: ${{ github.event_name != 'merge_group' && !startsWith(github.head_ref, 'release/') && github.event.pull_request.head.repo.full_name == 'smartcontractkit/chainlink' }} + name: Detect Changes runs-on: ubuntu-latest + outputs: + source: ${{ steps.files-changed.outputs.source }} + changesets: ${{ steps.files-changed.outputs.changesets }} + changeset_files: ${{ steps.files-changed.outputs.changeset_files }} steps: - name: Checkout the repo uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 - # We don't use detect-solidity-file-changes here because we need to use the "every" predicate quantifier - name: Filter paths uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 - id: changed_files + id: files-changed with: - list-files: "csv" + list-files: "json" # This is a valid input, see https://github.com/dorny/paths-filter/pull/226 predicate-quantifier: "every" filters: | - only_src_contracts: + source: - contracts/**/*.sol - '!contracts/**/*.t.sol' + changesets: + - 'contracts/.changeset/**' - - name: Collect Metrics - id: collect-gha-metrics - uses: smartcontractkit/push-gha-metrics-action@d9da21a2747016b3e13de58c7d4115a3d5c97935 # v3.0.1 - with: - id: solidity-jira - org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }} - basic-auth: ${{ secrets.GRAFANA_INTERNAL_BASIC_AUTH }} - hostname: ${{ secrets.GRAFANA_INTERNAL_HOST }} - this-job-name: Should Skip - continue-on-error: true - - enforce-jira-issue: - name: Enforce Jira Issue - runs-on: ubuntu-latest - # If a needs job is skipped, this job will be skipped and counted as successful - # The job skips on merge_group events, and any release branches - # Since we only want to enforce Jira issues on pull requests related to feature branches - needs: [skip-enforce-jira-issue] - # In addition to the above conditions, we only want to running on solidity related PRs. - # + enforce-traceability: # Note: A job that is skipped will report its status as "Success". # It will not prevent a pull request from merging, even if it is a required check. - if: ${{ needs.skip-enforce-jira-issue.outputs.should-enforce == 'true' }} + needs: [files-changed] + # We only want to run this job if the source files have changed + if: ${{ needs.files-changed.outputs.source == 'true' }} + name: Enforce Traceability + runs-on: ubuntu-latest steps: - name: Checkout the repo uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + - name: Make a comment + uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0 + with: + message: | + I see you updated files related to `contracts`. Please run `pnpm changeset` in the `contracts` directory to add a changeset. + reactions: eyes + comment_tag: changeset-contracts + # If the changeset is added, then we delete the comment, otherwise we add it. + mode: ${{ needs.files-changed.outputs.changeset == 'true' && 'delete' || 'upsert' }} + # We only create the comment if the changeset is not added + create_if_not_exists: ${{ needs.files-changed.outputs.changeset == 'true' && 'false' || 'true' }} + + - name: Check for new changeset for contracts + if: ${{ needs.files-changed.outputs.changeset == 'false' }} + shell: bash + run: | + echo "Please run pnpm changeset to add a changeset for contracts." + exit 1 + - name: Setup NodeJS uses: ./.github/actions/setup-nodejs @@ -92,9 +93,9 @@ jobs: id: collect-gha-metrics uses: smartcontractkit/push-gha-metrics-action@d9da21a2747016b3e13de58c7d4115a3d5c97935 # v3.0.1 with: - id: solidity-jira + id: soldity-traceability org-id: ${{ secrets.GRAFANA_INTERNAL_TENANT_ID }} basic-auth: ${{ secrets.GRAFANA_INTERNAL_BASIC_AUTH }} hostname: ${{ secrets.GRAFANA_INTERNAL_HOST }} - this-job-name: Enforce Jira Issue + this-job-name: Enforce Traceability continue-on-error: true From d5e936b8e8c97e74c82d9bf002a7504316744ba4 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Fri, 16 Aug 2024 17:15:08 -0700 Subject: [PATCH 02/32] Update solidity-jira to handle jira tracability as a whole --- .../jira/create-jira-traceability.test.ts | 18 +++ .../scripts/jira/create-jira-traceability.ts | 140 ++++++++++++++++++ .github/scripts/jira/enforce-jira-issue.ts | 41 ++++- .github/scripts/jira/lib.ts | 21 ++- .github/scripts/jira/package.json | 1 + .github/workflows/solidity-jira.yml | 22 ++- 6 files changed, 235 insertions(+), 8 deletions(-) create mode 100644 .github/scripts/jira/create-jira-traceability.test.ts create mode 100644 .github/scripts/jira/create-jira-traceability.ts diff --git a/.github/scripts/jira/create-jira-traceability.test.ts b/.github/scripts/jira/create-jira-traceability.test.ts new file mode 100644 index 00000000000..ebf7446b2d8 --- /dev/null +++ b/.github/scripts/jira/create-jira-traceability.test.ts @@ -0,0 +1,18 @@ +import {generateIssueLabel, generateJiraIssuesLink} from './create-jira-traceability'; +import {describe, it, expect} from 'vitest' + +describe('generateJiraIssuesLink', () => { + it('should generate a Jira issues link', () => { + const issues = ['KS-435', 'KS-434']; + expect(generateJiraIssuesLink(issues)).toMatchInlineSnapshot(`"https://smartcontract-it.atlassian.net/issues/?jql=issuekey+in+%28KS-435%2C+KS-434%29"`) + }); +}); + +describe('generateIssueLabel', () => { + it('should generate an issue label', () => { + const product = 'automation'; + const baseRef = '0de9b3b'; + const headRef = 'e5b3b9d'; + expect(generateIssueLabel(product, baseRef, headRef)).toMatchInlineSnapshot(`"review-artifacts-automation-base:0de9b3b-head:e5b3b9d"`) + }); +}) diff --git a/.github/scripts/jira/create-jira-traceability.ts b/.github/scripts/jira/create-jira-traceability.ts new file mode 100644 index 00000000000..a4d4741d41b --- /dev/null +++ b/.github/scripts/jira/create-jira-traceability.ts @@ -0,0 +1,140 @@ +import * as jira from "jira.js"; +import { createJiraClient, extractJiraIssueNumbersFrom } from "./lib"; +import * as core from "@actions/core"; +import { URL } from "url"; + +/** + * Adds traceability to JIRA issues by commenting on each issue with a link to the artifact payload + * along with a label to connect all issues to the same chainlink product review. + * + * @param client The jira client + * @param issues The list of JIRA issue numbers to add traceability to + * @param label The label to add to each issue + * @param artifactUrl The url to the artifact payload that we'll comment on each issue with + */ +export async function addTraceabillityToJiraIssues( + client: jira.Version3Client, + issues: string[], + label: string, + artifactUrl: string +) { + for (const issue of issues) { + await checkAndAddArtifactPayloadComment(client, issue, artifactUrl); + + // CHECK: We don't need to see if the label exists, should no-op + core.info(`Adding label ${label} to issue ${issue}`); + await client.issues.editIssue({ + issueIdOrKey: issue, + update: { + labels: [{ add: label }], + }, + }); + } +} + +/** + * Checks if the artifact payload already exists as a comment on the issue, if not, adds it. + */ +async function checkAndAddArtifactPayloadComment( + client: jira.Version3.Version3Client, + issue: string, + artifactUrl: string +) { + const maxResults = 5000; + const comments = await client.issueComments.getComments({ + issueIdOrKey: issue, + maxResults, // this is the default maxResults, see https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-get + }); + if (comments.total ?? 0 > maxResults) { + throw Error( + `Too many comments on issue ${issue}, please increase maxResults` + ); + } + const commentExists = comments.comments?.some((c) => + c?.body?.text?.includes(artifactUrl) + ); + + if (commentExists) { + core.info(`Artifact payload already exists as comment on issue, skipping`); + } else { + core.info(`Adding artifact payload as comment on issue ${issue}`); + await client.issueComments.addComment({ + issueIdOrKey: issue, + comment: `:link: [Artifact Payload](${artifactUrl})`, + }); + } +} + +function fetchEnvironmentVariables() { + const product = process.env.CHAINLINK_PRODUCT; + if (!product) { + throw Error("CHAINLINK_PRODUCT environment variable is missing"); + } + const baseRef = process.env.BASE_REF; + if (!baseRef) { + throw Error("BASE_REF environment variable is missing"); + } + const headRef = process.env.HEAD_REF; + if (!headRef) { + throw Error("HEAD_REF environment variable is missing"); + } + + const artifactUrl = process.env.ARTIFACT_URL; + if (!artifactUrl) { + throw Error("ARTIFACT_URL environment variable is missing"); + } + return { product, baseRef, headRef, artifactUrl }; +} + +function extractChangesetFiles(): string[] { + const changesetFiles = process.env.CHANGESET_FILES; + if (!changesetFiles) { + throw Error("Missing required environment variable CHANGESET_FILES"); + } + const parsedChangesetFiles = JSON.parse(changesetFiles); + if (parsedChangesetFiles.length === 0) { + throw Error("At least one changeset file must exist"); + } + + core.info(`Changeset to extract issues from: ${parsedChangesetFiles.join(", ")}`); + return parsedChangesetFiles; +} + +export function generateJiraIssuesLink(issues: string[]) { + // https://smartcontract-it.atlassian.net/issues/?jql=issuekey%20in%20%28KS-435%2C%20KS-434%29 + const baseUrl = "https://smartcontract-it.atlassian.net/issues/"; + const jqlQuery = `issuekey in (${issues.join(", ")})`; + const fullUrl = new URL(baseUrl); + fullUrl.searchParams.set("jql", jqlQuery); + + const urlStr = fullUrl.toString(); + core.info(`Jira issues link: ${urlStr}`); + return urlStr +} + +export function generateIssueLabel(product: string, baseRef: string, headRef: string) { + return `review-artifacts-${product}-base:${baseRef}-head:${headRef}`; +} + +/** + * For all affected jira issues listed within the changeset files supplied, + * we update each jira issue so that they are all labelled and have a comment linking them + * to the relevant artifact URL. + */ +export async function main() { + const { product, baseRef, headRef, artifactUrl } = + fetchEnvironmentVariables(); + const changesetFiles = extractChangesetFiles(); + const jiraIssueNumbers = await extractJiraIssueNumbersFrom(changesetFiles); + + const client = createJiraClient(); + const label = generateIssueLabel(product, baseRef, headRef); + await addTraceabillityToJiraIssues( + client, + jiraIssueNumbers, + label, + artifactUrl + ); + + core.setOutput("jira-issues-link", generateJiraIssuesLink(jiraIssueNumbers)); +} diff --git a/.github/scripts/jira/enforce-jira-issue.ts b/.github/scripts/jira/enforce-jira-issue.ts index e0054b25d0e..c11d996b7dd 100644 --- a/.github/scripts/jira/enforce-jira-issue.ts +++ b/.github/scripts/jira/enforce-jira-issue.ts @@ -1,6 +1,7 @@ import * as core from "@actions/core"; import jira from "jira.js"; import { createJiraClient, parseIssueNumberFrom } from "./lib"; +import { promises as fs } from "fs"; async function doesIssueExist( client: jira.Version3Client, @@ -44,6 +45,8 @@ async function main() { const commitMessage = process.env.COMMIT_MESSAGE; const branchName = process.env.BRANCH_NAME; const dryRun = !!process.env.DRY_RUN; + const { changesetFile } = extractChangesetFile(); + const client = createJiraClient(); // Checks for the Jira issue number and exit if it can't find it @@ -58,9 +61,45 @@ async function main() { const exists = await doesIssueExist(client, issueNumber, dryRun); if (!exists) { - core.setFailed(`JIRA issue ${issueNumber} not found, this pull request must be associated with a JIRA issue.`); + core.setFailed( + `JIRA issue ${issueNumber} not found, this pull request must be associated with a JIRA issue.` + ); + return; + } + + core.info(`Appending JIRA issue ${issueNumber} to changeset file`); + await appendIssueNumberToChangesetFile(changesetFile, issueNumber); +} + +async function appendIssueNumberToChangesetFile( + changesetFile: string, + issueNumber: string +) { + const changesetContents = await fs.readFile(changesetFile, "utf-8"); + // Check if the issue number is already in the changeset file + if (changesetContents.includes(issueNumber)) { + core.info("Issue number already exists in changeset file, skipping..."); return; } + + const updatedChangesetContents = `${issueNumber}\n${changesetContents}`; + await fs.writeFile(changesetFile, updatedChangesetContents); +} + +function extractChangesetFile() { + const changesetFiles = process.env.CHANGESET_FILES; + if (!changesetFiles) { + throw Error("Missing required environment variable CHANGESET_FILES"); + } + const parsedChangesetFiles = JSON.parse(changesetFiles); + if (parsedChangesetFiles.length !== 1) { + throw Error( + "This action only supports one changeset file per pull request." + ); + } + const [changesetFile] = parsedChangesetFiles; + + return { changesetFile }; } async function run() { diff --git a/.github/scripts/jira/lib.ts b/.github/scripts/jira/lib.ts index 72f1d57966c..29793b25c68 100644 --- a/.github/scripts/jira/lib.ts +++ b/.github/scripts/jira/lib.ts @@ -1,6 +1,6 @@ - -import * as core from '@actions/core' -import * as jira from 'jira.js' +import { readFile } from "fs/promises"; +import * as core from "@actions/core"; +import * as jira from "jira.js"; /** * Given a list of strings, this function will return the first JIRA issue number it finds. @@ -24,6 +24,21 @@ export function parseIssueNumberFrom( return parsed[0]; } +export async function extractJiraIssueNumbersFrom(filePaths: string[]) { + const issueNumbers: string[] = []; + + for (const path of filePaths) { + const content = await readFile(path, "utf-8"); + const issueNumber = parseIssueNumberFrom(content); + if (issueNumber) { + issueNumbers.push(issueNumber); + } + } + + return issueNumbers; +} + + /** * Converts an array of tags to an array of labels. * diff --git a/.github/scripts/jira/package.json b/.github/scripts/jira/package.json index 95bfbb1e486..7d9e68c6412 100644 --- a/.github/scripts/jira/package.json +++ b/.github/scripts/jira/package.json @@ -15,6 +15,7 @@ "scripts": { "issue:update": "tsx update-jira-issue.ts", "issue:enforce": "tsx enforce-jira-issue.ts", + "issue:traceability": "tsx create-jira-traceabililty.ts", "test": "vitest" }, "dependencies": { diff --git a/.github/workflows/solidity-jira.yml b/.github/workflows/solidity-jira.yml index f15e05884e9..c08e1d6c3ca 100644 --- a/.github/workflows/solidity-jira.yml +++ b/.github/workflows/solidity-jira.yml @@ -76,18 +76,32 @@ jobs: working-directory: ./.github/scripts/jira run: pnpm i - - name: Enforce Jira Issue + # Because of our earlier checks, we know that both the source and changeset files have changed + - name: Enforce Traceability working-directory: ./.github/scripts/jira run: | echo "COMMIT_MESSAGE=$(git log -1 --pretty=format:'%s')" >> $GITHUB_ENV pnpm issue:enforce env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CHANGESET_FILES: ${{ needs.files-changed.outputs.changeset_files }} + + PR_TITLE: ${{ github.event.pull_request.title }} + BRANCH_NAME: ${{ github.event.pull_request.head.ref }} + JIRA_HOST: ${{ secrets.JIRA_HOST }} JIRA_USERNAME: ${{ secrets.JIRA_USERNAME }} JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} - PR_TITLE: ${{ github.event.pull_request.title }} - BRANCH_NAME: ${{ github.event.pull_request.head.ref }} + + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Commit appended changeset file back to repo + - uses: planetscale/ghcommit-action@v0.1.6 + with: + commit_message: "Update changeset file with jira issue" + repo: ${{ github.repository }} + branch: ${{ github.head_ref }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Collect Metrics id: collect-gha-metrics From 50ab692c81984356daf3fe4fe57d05fb4e98ccde Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 22 Aug 2024 14:04:36 -0700 Subject: [PATCH 03/32] Fix changesets output typo --- .github/workflows/solidity-jira.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/solidity-jira.yml b/.github/workflows/solidity-jira.yml index c08e1d6c3ca..18ac9583e63 100644 --- a/.github/workflows/solidity-jira.yml +++ b/.github/workflows/solidity-jira.yml @@ -19,7 +19,7 @@ jobs: outputs: source: ${{ steps.files-changed.outputs.source }} changesets: ${{ steps.files-changed.outputs.changesets }} - changeset_files: ${{ steps.files-changed.outputs.changeset_files }} + changesets_files: ${{ steps.files-changed.outputs.changesets_files }} steps: - name: Checkout the repo uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 @@ -58,12 +58,12 @@ jobs: reactions: eyes comment_tag: changeset-contracts # If the changeset is added, then we delete the comment, otherwise we add it. - mode: ${{ needs.files-changed.outputs.changeset == 'true' && 'delete' || 'upsert' }} + mode: ${{ needs.files-changed.outputs.changesets == 'true' && 'delete' || 'upsert' }} # We only create the comment if the changeset is not added - create_if_not_exists: ${{ needs.files-changed.outputs.changeset == 'true' && 'false' || 'true' }} + create_if_not_exists: ${{ needs.files-changed.outputs.changesets == 'true' && 'false' || 'true' }} - name: Check for new changeset for contracts - if: ${{ needs.files-changed.outputs.changeset == 'false' }} + if: ${{ needs.files-changed.outputs.changesets == 'false' }} shell: bash run: | echo "Please run pnpm changeset to add a changeset for contracts." @@ -83,7 +83,7 @@ jobs: echo "COMMIT_MESSAGE=$(git log -1 --pretty=format:'%s')" >> $GITHUB_ENV pnpm issue:enforce env: - CHANGESET_FILES: ${{ needs.files-changed.outputs.changeset_files }} + CHANGESET_FILES: ${{ needs.files-changed.outputs.changesets_files }} PR_TITLE: ${{ github.event.pull_request.title }} BRANCH_NAME: ${{ github.event.pull_request.head.ref }} From 8accdb9de65f5c63939ce7bb40439a5509e53f13 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 22 Aug 2024 14:43:05 -0700 Subject: [PATCH 04/32] Make file paths absolute --- .github/scripts/jira/enforce-jira-issue.ts | 9 ++-- .github/scripts/jira/lib.test.ts | 50 +++++++++++++++++++++- .github/scripts/jira/lib.ts | 32 ++++++++++++-- 3 files changed, 83 insertions(+), 8 deletions(-) diff --git a/.github/scripts/jira/enforce-jira-issue.ts b/.github/scripts/jira/enforce-jira-issue.ts index c11d996b7dd..eac1647b829 100644 --- a/.github/scripts/jira/enforce-jira-issue.ts +++ b/.github/scripts/jira/enforce-jira-issue.ts @@ -1,7 +1,8 @@ import * as core from "@actions/core"; import jira from "jira.js"; -import { createJiraClient, parseIssueNumberFrom } from "./lib"; +import { createJiraClient, getGitTopLevel, parseIssueNumberFrom } from "./lib"; import { promises as fs } from "fs"; +import { join } from "path"; async function doesIssueExist( client: jira.Version3Client, @@ -75,7 +76,9 @@ async function appendIssueNumberToChangesetFile( changesetFile: string, issueNumber: string ) { - const changesetContents = await fs.readFile(changesetFile, "utf-8"); + const gitTopLevel = await getGitTopLevel(); + const fullChangesetPath = join(gitTopLevel, changesetFile); + const changesetContents = await fs.readFile(fullChangesetPath, "utf-8"); // Check if the issue number is already in the changeset file if (changesetContents.includes(issueNumber)) { core.info("Issue number already exists in changeset file, skipping..."); @@ -83,7 +86,7 @@ async function appendIssueNumberToChangesetFile( } const updatedChangesetContents = `${issueNumber}\n${changesetContents}`; - await fs.writeFile(changesetFile, updatedChangesetContents); + await fs.writeFile(fullChangesetPath, updatedChangesetContents); } function extractChangesetFile() { diff --git a/.github/scripts/jira/lib.test.ts b/.github/scripts/jira/lib.test.ts index 9c751e84088..4ca8d4923e4 100644 --- a/.github/scripts/jira/lib.test.ts +++ b/.github/scripts/jira/lib.test.ts @@ -1,5 +1,6 @@ -import { expect, describe, it } from "vitest"; -import { parseIssueNumberFrom, tagsToLabels } from "./lib"; +import { expect, describe, it, vi } from "vitest"; +import { getGitTopLevel, parseIssueNumberFrom, tagsToLabels } from "./lib"; +import * as core from "@actions/core"; describe("parseIssueNumberFrom", () => { it("should return the first JIRA issue number found", () => { @@ -45,3 +46,48 @@ describe("tagsToLabels", () => { ]); }); }); + + const mockExecPromise = vi.fn() + vi.mock("util", () => ({ + promisify: () => mockExecPromise, + })); + +describe("getGitTopLevel", () => { + it("should log the top-level directory when git command succeeds", async () => { + mockExecPromise.mockResolvedValueOnce({ + stdout: "/path/to/top-level-dir", + stderr: "", + }); + + const mockConsoleLog = vi.spyOn(core, "info"); + await getGitTopLevel(); + + expect(mockExecPromise).toHaveBeenCalledWith("git rev-parse --show-toplevel"); + expect(mockConsoleLog).toHaveBeenCalledWith("Top-level directory: /path/to/top-level-dir"); + }); + + it("should log an error message when git command fails", async () => { + mockExecPromise.mockRejectedValueOnce({ + message: "Command failed", + }); + + const mockConsoleError = vi.spyOn(core, "error"); + await getGitTopLevel().catch(() => {}); + + expect(mockExecPromise).toHaveBeenCalledWith("git rev-parse --show-toplevel"); + expect(mockConsoleError).toHaveBeenCalledWith("Error executing command: Command failed"); + }); + + it("should log an error message when git command output contains an error", async () => { + mockExecPromise.mockResolvedValueOnce({ + stdout: "", + stderr: "Error: Command failed", + }); + + const mockConsoleError = vi.spyOn(core, "error"); + await getGitTopLevel().catch(() => {}); + + expect(mockExecPromise).toHaveBeenCalledWith("git rev-parse --show-toplevel"); + expect(mockConsoleError).toHaveBeenCalledWith("Error in command output: Error: Command failed"); + }); +}); diff --git a/.github/scripts/jira/lib.ts b/.github/scripts/jira/lib.ts index 29793b25c68..0028a06d18f 100644 --- a/.github/scripts/jira/lib.ts +++ b/.github/scripts/jira/lib.ts @@ -1,6 +1,32 @@ import { readFile } from "fs/promises"; import * as core from "@actions/core"; import * as jira from "jira.js"; +import { exec } from "child_process"; +import { promisify } from "util"; +import { join } from "path"; + +export async function getGitTopLevel(): Promise { + const execPromise = promisify(exec); + try { + const { stdout, stderr } = await execPromise( + "git rev-parse --show-toplevel" + ); + + if (stderr) { + const msg = `Error in command output: ${stderr}`; + core.error(msg); + throw Error(msg); + } + + const topLevelDir = stdout.trim(); + core.info(`Top-level directory: ${topLevelDir}`); + return topLevelDir; + } catch (error) { + const msg = `Error executing command: ${(error as any).message}`; + core.error(msg); + throw Error(msg); + } +} /** * Given a list of strings, this function will return the first JIRA issue number it finds. @@ -26,9 +52,10 @@ export function parseIssueNumberFrom( export async function extractJiraIssueNumbersFrom(filePaths: string[]) { const issueNumbers: string[] = []; - + const gitTopLevel = await getGitTopLevel(); for (const path of filePaths) { - const content = await readFile(path, "utf-8"); + const fullPath = join(gitTopLevel, path); + const content = await readFile(fullPath, "utf-8"); const issueNumber = parseIssueNumberFrom(content); if (issueNumber) { issueNumbers.push(issueNumber); @@ -38,7 +65,6 @@ export async function extractJiraIssueNumbersFrom(filePaths: string[]) { return issueNumbers; } - /** * Converts an array of tags to an array of labels. * From 0a1dfc8b2a40381f32f874cc099c7c707a9c8854 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 22 Aug 2024 14:49:18 -0700 Subject: [PATCH 05/32] Prevent merge commits from being added by auto commit action --- .github/workflows/solidity-jira.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/solidity-jira.yml b/.github/workflows/solidity-jira.yml index 18ac9583e63..adf1025c9db 100644 --- a/.github/workflows/solidity-jira.yml +++ b/.github/workflows/solidity-jira.yml @@ -47,8 +47,14 @@ jobs: name: Enforce Traceability runs-on: ubuntu-latest steps: + # https://github.com/planetscale/ghcommit-action/blob/c7915d6c18d5ce4eb42b0eff3f10a29fe0766e4c/README.md?plain=1#L41 + # + # Include the pull request ref in the checkout action to prevent merge commit + # https://github.com/actions/checkout?tab=readme-ov-file#checkout-pull-request-head-commit-instead-of-merge-commit - name: Checkout the repo uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Make a comment uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0 From 2ec5506886535dc24418a718a5bfcfff851fb606 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 22 Aug 2024 14:51:10 -0700 Subject: [PATCH 06/32] Add issue number to tail of document rather than head --- .github/scripts/jira/enforce-jira-issue.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/jira/enforce-jira-issue.ts b/.github/scripts/jira/enforce-jira-issue.ts index eac1647b829..9d8c6e490ee 100644 --- a/.github/scripts/jira/enforce-jira-issue.ts +++ b/.github/scripts/jira/enforce-jira-issue.ts @@ -85,7 +85,7 @@ async function appendIssueNumberToChangesetFile( return; } - const updatedChangesetContents = `${issueNumber}\n${changesetContents}`; + const updatedChangesetContents = `${changesetContents}\n\n${issueNumber}`; await fs.writeFile(fullChangesetPath, updatedChangesetContents); } From 5b7a182abb4b977592813b211e711f705a89af08 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 22 Aug 2024 14:53:35 -0700 Subject: [PATCH 07/32] Tighten up file pattern for committing --- .github/workflows/solidity-jira.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/solidity-jira.yml b/.github/workflows/solidity-jira.yml index adf1025c9db..1479a054da9 100644 --- a/.github/workflows/solidity-jira.yml +++ b/.github/workflows/solidity-jira.yml @@ -106,6 +106,7 @@ jobs: commit_message: "Update changeset file with jira issue" repo: ${{ github.repository }} branch: ${{ github.head_ref }} + file_pattern: "contracts/.changeset/*" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 95319fb1d8d4f6fa72a9767be6580fce8bfe8079 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 22 Aug 2024 14:53:58 -0700 Subject: [PATCH 08/32] Add GATI so workflows run on auto commits --- .github/workflows/solidity-jira.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/solidity-jira.yml b/.github/workflows/solidity-jira.yml index 1479a054da9..eb3ee35a6f5 100644 --- a/.github/workflows/solidity-jira.yml +++ b/.github/workflows/solidity-jira.yml @@ -46,6 +46,10 @@ jobs: if: ${{ needs.files-changed.outputs.source == 'true' }} name: Enforce Traceability runs-on: ubuntu-latest + permissions: + actions: read + id-token: write + contents: read steps: # https://github.com/planetscale/ghcommit-action/blob/c7915d6c18d5ce4eb42b0eff3f10a29fe0766e4c/README.md?plain=1#L41 # @@ -56,6 +60,14 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} + - name: Assume role capable of dispatching action + uses: smartcontractkit/.github/actions/setup-github-token@ef78fa97bf3c77de6563db1175422703e9e6674f # setup-github-token@0.2.1 + id: get-gh-token + with: + aws-role-arn: ${{ secrets.AWS_OIDC_CHAINLINK_CI_AUTO_PR_TOKEN_ISSUER_ROLE_ARN }} + aws-lambda-url: ${{ secrets.AWS_INFRA_RELENG_TOKEN_ISSUER_LAMBDA_URL }} + aws-region: ${{ secrets.AWS_REGION }} + - name: Make a comment uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0 with: @@ -108,7 +120,7 @@ jobs: branch: ${{ github.head_ref }} file_pattern: "contracts/.changeset/*" env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ steps.get-gh-token.outputs.access-token }} - name: Collect Metrics id: collect-gha-metrics From 3d02dfcbaf6fbad8db723e84571fe5b3a9e5df7a Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 22 Aug 2024 15:40:43 -0700 Subject: [PATCH 09/32] Modify workflow to generate jira traceabillity --- .../workflows/solidity-foundry-artifacts.yml | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/.github/workflows/solidity-foundry-artifacts.yml b/.github/workflows/solidity-foundry-artifacts.yml index 061caf1ea7f..2f568b51e30 100644 --- a/.github/workflows/solidity-foundry-artifacts.yml +++ b/.github/workflows/solidity-foundry-artifacts.yml @@ -129,6 +129,7 @@ jobs: with: working-directory: contracts + # THIS IS BROKEN - name: Copy modified changesets if: ${{ needs.changes.outputs.changeset_changes == 'true' }} run: | @@ -361,6 +362,7 @@ jobs: name: tmp-* - name: Print Artifact URL in job summary + id: gather-all-artifacts env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | @@ -372,7 +374,47 @@ jobs: echo "Product: **${{ inputs.product }}**" >> $GITHUB_STEP_SUMMARY echo "Base Ref used: **${{ inputs.base_ref }}**" >> $GITHUB_STEP_SUMMARY echo "Commit SHA used: **${{ inputs.commit_to_use || github.sha }}**" >> $GITHUB_STEP_SUMMARY - echo "[Artifact URL](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts/$ARTIFACT_ID)" >> $GITHUB_STEP_SUMMARY + + artifact_url="[Artifact URL](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts/$ARTIFACT_ID)" + echo "$artifact_url" >> $GITHUB_STEP_SUMMARY + echo "artifact-url=$artifact_url" >> $GITHUB_OUTPUT + + - name: Setup NodeJS + uses: ./.github/actions/setup-nodejs + + - name: Setup Jira + working-directory: ./.github/scripts/jira + run: pnpm i + + # For testing, since we need JSON output not CSV + - name: Find modified contracts + uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + id: changes + with: + list-files: 'json' + base: ${{ inputs.base_ref }} + predicate-quantifier: every + filters: | + changeset: + - modified|added: 'contracts/.changeset/!(README)*.md' + + # Because of our earlier checks, we know that both the source and changeset files have changed + - name: Enforce Traceability + working-directory: ./.github/scripts/jira + run: | + pnpm issue:traceability + env: + CHANGESET_FILES: ${{ steps.changes.outputs.changeset_files }} + CHAINLINK_PRODUCT: ${{ inputs.product }} + BASE_REF: ${{ inputs.base_ref }} + HEAD_REF: ${{ inputs.commit_to_use || github.sha }} + ARTIFACT_URL: ${{ steps.gather-all-artifacts.outputs.artifact-url }} + + JIRA_HOST: ${{ secrets.JIRA_HOST }} + JIRA_USERNAME: ${{ secrets.JIRA_USERNAME }} + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} + + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} notify-no-changes: if: ${{ needs.changes.outputs.product_changes == 'false' }} From eb353dc176327296ca5f7f5e0ef1e5412d02f638 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 22 Aug 2024 15:53:10 -0700 Subject: [PATCH 10/32] Add checkout --- .github/workflows/solidity-foundry-artifacts.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/solidity-foundry-artifacts.yml b/.github/workflows/solidity-foundry-artifacts.yml index 2f568b51e30..23fccca319c 100644 --- a/.github/workflows/solidity-foundry-artifacts.yml +++ b/.github/workflows/solidity-foundry-artifacts.yml @@ -344,6 +344,11 @@ jobs: runs-on: ubuntu-latest needs: [coverage-and-book, uml-static-analysis, gather-basic-info, changes] steps: + - name: Checkout the repo + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + with: + ref: ${{ inputs.commit_to_use || github.sha }} + - name: Download all artifacts uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 with: From d91b90946e58889bc04f066b47d81a4a18a56de2 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 22 Aug 2024 16:00:55 -0700 Subject: [PATCH 11/32] Fix typo --- .github/scripts/jira/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/jira/package.json b/.github/scripts/jira/package.json index 7d9e68c6412..94a805314af 100644 --- a/.github/scripts/jira/package.json +++ b/.github/scripts/jira/package.json @@ -15,7 +15,7 @@ "scripts": { "issue:update": "tsx update-jira-issue.ts", "issue:enforce": "tsx enforce-jira-issue.ts", - "issue:traceability": "tsx create-jira-traceabililty.ts", + "issue:traceability": "tsx create-jira-traceability.ts", "test": "vitest" }, "dependencies": { From c30a37c676b5e4927925b2be2e4edc351c94f15f Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 22 Aug 2024 16:14:37 -0700 Subject: [PATCH 12/32] Add better naming for job --- .github/workflows/solidity-foundry-artifacts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/solidity-foundry-artifacts.yml b/.github/workflows/solidity-foundry-artifacts.yml index 23fccca319c..43b71f500e5 100644 --- a/.github/workflows/solidity-foundry-artifacts.yml +++ b/.github/workflows/solidity-foundry-artifacts.yml @@ -404,7 +404,7 @@ jobs: - modified|added: 'contracts/.changeset/!(README)*.md' # Because of our earlier checks, we know that both the source and changeset files have changed - - name: Enforce Traceability + - name: Create Traceability working-directory: ./.github/scripts/jira run: | pnpm issue:traceability From a42f66b2fe1d6e3e84b14b5c2966cbf92cd01a4c Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 22 Aug 2024 16:14:45 -0700 Subject: [PATCH 13/32] Add more logging --- .github/scripts/jira/create-jira-traceability.ts | 1 + .github/scripts/jira/lib.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.github/scripts/jira/create-jira-traceability.ts b/.github/scripts/jira/create-jira-traceability.ts index a4d4741d41b..2a6dff94b52 100644 --- a/.github/scripts/jira/create-jira-traceability.ts +++ b/.github/scripts/jira/create-jira-traceability.ts @@ -125,6 +125,7 @@ export async function main() { const { product, baseRef, headRef, artifactUrl } = fetchEnvironmentVariables(); const changesetFiles = extractChangesetFiles(); + core.info(`Extracting Jira issue numbers from changeset files: ${changesetFiles.join(", ")}`); const jiraIssueNumbers = await extractJiraIssueNumbersFrom(changesetFiles); const client = createJiraClient(); diff --git a/.github/scripts/jira/lib.ts b/.github/scripts/jira/lib.ts index 0028a06d18f..615bde96aff 100644 --- a/.github/scripts/jira/lib.ts +++ b/.github/scripts/jira/lib.ts @@ -53,10 +53,13 @@ export function parseIssueNumberFrom( export async function extractJiraIssueNumbersFrom(filePaths: string[]) { const issueNumbers: string[] = []; const gitTopLevel = await getGitTopLevel(); + for (const path of filePaths) { const fullPath = join(gitTopLevel, path); + core.info(`Reading file: ${fullPath}`); const content = await readFile(fullPath, "utf-8"); const issueNumber = parseIssueNumberFrom(content); + core.info(`Extracted issue number: ${issueNumber}`); if (issueNumber) { issueNumbers.push(issueNumber); } From 7c4aae4f68efa4f0b71c476569f3f61ebd8f7aeb Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 22 Aug 2024 16:23:03 -0700 Subject: [PATCH 14/32] Append to step summary from within script --- .github/scripts/jira/create-jira-traceability.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/scripts/jira/create-jira-traceability.ts b/.github/scripts/jira/create-jira-traceability.ts index 2a6dff94b52..cdf3fae4eeb 100644 --- a/.github/scripts/jira/create-jira-traceability.ts +++ b/.github/scripts/jira/create-jira-traceability.ts @@ -121,7 +121,7 @@ export function generateIssueLabel(product: string, baseRef: string, headRef: st * we update each jira issue so that they are all labelled and have a comment linking them * to the relevant artifact URL. */ -export async function main() { +async function main() { const { product, baseRef, headRef, artifactUrl } = fetchEnvironmentVariables(); const changesetFiles = extractChangesetFiles(); @@ -137,5 +137,6 @@ export async function main() { artifactUrl ); - core.setOutput("jira-issues-link", generateJiraIssuesLink(jiraIssueNumbers)); + core.summary.addLink("Jira Issues", generateJiraIssuesLink(jiraIssueNumbers)); } + main() From e49d519b81d0529e61940567273d8876894b930d Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 22 Aug 2024 17:55:34 -0700 Subject: [PATCH 15/32] Extract functions and use labels over individual issues --- .../jira/create-jira-traceability.test.ts | 18 ------- .../scripts/jira/create-jira-traceability.ts | 47 +++++++++---------- .github/scripts/jira/lib.test.ts | 17 ++++++- .github/scripts/jira/lib.ts | 20 ++++++++ 4 files changed, 57 insertions(+), 45 deletions(-) delete mode 100644 .github/scripts/jira/create-jira-traceability.test.ts diff --git a/.github/scripts/jira/create-jira-traceability.test.ts b/.github/scripts/jira/create-jira-traceability.test.ts deleted file mode 100644 index ebf7446b2d8..00000000000 --- a/.github/scripts/jira/create-jira-traceability.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import {generateIssueLabel, generateJiraIssuesLink} from './create-jira-traceability'; -import {describe, it, expect} from 'vitest' - -describe('generateJiraIssuesLink', () => { - it('should generate a Jira issues link', () => { - const issues = ['KS-435', 'KS-434']; - expect(generateJiraIssuesLink(issues)).toMatchInlineSnapshot(`"https://smartcontract-it.atlassian.net/issues/?jql=issuekey+in+%28KS-435%2C+KS-434%29"`) - }); -}); - -describe('generateIssueLabel', () => { - it('should generate an issue label', () => { - const product = 'automation'; - const baseRef = '0de9b3b'; - const headRef = 'e5b3b9d'; - expect(generateIssueLabel(product, baseRef, headRef)).toMatchInlineSnapshot(`"review-artifacts-automation-base:0de9b3b-head:e5b3b9d"`) - }); -}) diff --git a/.github/scripts/jira/create-jira-traceability.ts b/.github/scripts/jira/create-jira-traceability.ts index cdf3fae4eeb..0dbfce89331 100644 --- a/.github/scripts/jira/create-jira-traceability.ts +++ b/.github/scripts/jira/create-jira-traceability.ts @@ -1,7 +1,11 @@ import * as jira from "jira.js"; -import { createJiraClient, extractJiraIssueNumbersFrom } from "./lib"; +import { + createJiraClient, + extractJiraIssueNumbersFrom, + generateIssueLabel, + generateJiraIssuesLink, +} from "./lib"; import * as core from "@actions/core"; -import { URL } from "url"; /** * Adds traceability to JIRA issues by commenting on each issue with a link to the artifact payload @@ -12,7 +16,7 @@ import { URL } from "url"; * @param label The label to add to each issue * @param artifactUrl The url to the artifact payload that we'll comment on each issue with */ -export async function addTraceabillityToJiraIssues( +async function addTraceabillityToJiraIssues( client: jira.Version3Client, issues: string[], label: string, @@ -45,9 +49,10 @@ async function checkAndAddArtifactPayloadComment( issueIdOrKey: issue, maxResults, // this is the default maxResults, see https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-get }); - if (comments.total ?? 0 > maxResults) { + core.debug(JSON.stringify(comments.comments)); + if ((comments.total ?? 0) > maxResults) { throw Error( - `Too many comments on issue ${issue}, please increase maxResults` + `Too many (${comments.total}) comments on issue ${issue}, please increase maxResults (${maxResults})` ); } const commentExists = comments.comments?.some((c) => @@ -60,7 +65,7 @@ async function checkAndAddArtifactPayloadComment( core.info(`Adding artifact payload as comment on issue ${issue}`); await client.issueComments.addComment({ issueIdOrKey: issue, - comment: `:link: [Artifact Payload](${artifactUrl})`, + comment: artifactUrl, }); } } @@ -96,26 +101,12 @@ function extractChangesetFiles(): string[] { throw Error("At least one changeset file must exist"); } - core.info(`Changeset to extract issues from: ${parsedChangesetFiles.join(", ")}`); + core.info( + `Changeset to extract issues from: ${parsedChangesetFiles.join(", ")}` + ); return parsedChangesetFiles; } -export function generateJiraIssuesLink(issues: string[]) { - // https://smartcontract-it.atlassian.net/issues/?jql=issuekey%20in%20%28KS-435%2C%20KS-434%29 - const baseUrl = "https://smartcontract-it.atlassian.net/issues/"; - const jqlQuery = `issuekey in (${issues.join(", ")})`; - const fullUrl = new URL(baseUrl); - fullUrl.searchParams.set("jql", jqlQuery); - - const urlStr = fullUrl.toString(); - core.info(`Jira issues link: ${urlStr}`); - return urlStr -} - -export function generateIssueLabel(product: string, baseRef: string, headRef: string) { - return `review-artifacts-${product}-base:${baseRef}-head:${headRef}`; -} - /** * For all affected jira issues listed within the changeset files supplied, * we update each jira issue so that they are all labelled and have a comment linking them @@ -125,7 +116,11 @@ async function main() { const { product, baseRef, headRef, artifactUrl } = fetchEnvironmentVariables(); const changesetFiles = extractChangesetFiles(); - core.info(`Extracting Jira issue numbers from changeset files: ${changesetFiles.join(", ")}`); + core.info( + `Extracting Jira issue numbers from changeset files: ${changesetFiles.join( + ", " + )}` + ); const jiraIssueNumbers = await extractJiraIssueNumbersFrom(changesetFiles); const client = createJiraClient(); @@ -137,6 +132,6 @@ async function main() { artifactUrl ); - core.summary.addLink("Jira Issues", generateJiraIssuesLink(jiraIssueNumbers)); + core.summary.addLink("Jira Issues", generateJiraIssuesLink(label)); } - main() +main(); diff --git a/.github/scripts/jira/lib.test.ts b/.github/scripts/jira/lib.test.ts index 4ca8d4923e4..e7d5c00818e 100644 --- a/.github/scripts/jira/lib.test.ts +++ b/.github/scripts/jira/lib.test.ts @@ -1,5 +1,5 @@ import { expect, describe, it, vi } from "vitest"; -import { getGitTopLevel, parseIssueNumberFrom, tagsToLabels } from "./lib"; +import { generateIssueLabel, generateJiraIssuesLink, getGitTopLevel, parseIssueNumberFrom, tagsToLabels } from "./lib"; import * as core from "@actions/core"; describe("parseIssueNumberFrom", () => { @@ -91,3 +91,18 @@ describe("getGitTopLevel", () => { expect(mockConsoleError).toHaveBeenCalledWith("Error in command output: Error: Command failed"); }); }); + +describe('generateJiraIssuesLink', () => { + it('should generate a Jira issues link', () => { + expect(generateJiraIssuesLink("review-artifacts-automation-base:0de9b3b-head:e5b3b9d")).toMatchInlineSnapshot(`"https://smartcontract-it.atlassian.net/issues/?jql=labels+%3D+%22review-artifacts-automation-base%3A0de9b3b-head%3Ae5b3b9d%22"`) + }); +}); + +describe('generateIssueLabel', () => { + it('should generate an issue label', () => { + const product = 'automation'; + const baseRef = '0de9b3b'; + const headRef = 'e5b3b9d'; + expect(generateIssueLabel(product, baseRef, headRef)).toMatchInlineSnapshot(`"review-artifacts-automation-base:0de9b3b-head:e5b3b9d"`) + }); +}) diff --git a/.github/scripts/jira/lib.ts b/.github/scripts/jira/lib.ts index 615bde96aff..61a48a892a5 100644 --- a/.github/scripts/jira/lib.ts +++ b/.github/scripts/jira/lib.ts @@ -5,6 +5,26 @@ import { exec } from "child_process"; import { promisify } from "util"; import { join } from "path"; +export function generateJiraIssuesLink(label: string) { + // https://smartcontract-it.atlassian.net/issues/?jql=labels%20%3D%20%22review-artifacts-automation-base%3A8d818ea265ff08887e61ace4f83364a3ee149ef0-head%3A3c45b71f3610de28f429cef0163936eaa448e63c%22 + const baseUrl = "https://smartcontract-it.atlassian.net/issues/"; + const jqlQuery = `labels = "${label}"`; + const fullUrl = new URL(baseUrl); + fullUrl.searchParams.set("jql", jqlQuery); + + const urlStr = fullUrl.toString(); + core.info(`Jira issues link: ${urlStr}`); + return urlStr; +} + +export function generateIssueLabel( + product: string, + baseRef: string, + headRef: string +) { + return `review-artifacts-${product}-base:${baseRef}-head:${headRef}`; +} + export async function getGitTopLevel(): Promise { const execPromise = promisify(exec); try { From 13dcfc8bed8f6e4a5a6bf93a596fa3521d02231d Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 22 Aug 2024 18:23:58 -0700 Subject: [PATCH 16/32] Fix comments handling --- .../scripts/jira/create-jira-traceability.ts | 71 +++++++++++++++++-- 1 file changed, 64 insertions(+), 7 deletions(-) diff --git a/.github/scripts/jira/create-jira-traceability.ts b/.github/scripts/jira/create-jira-traceability.ts index 0dbfce89331..3f1cca7d812 100644 --- a/.github/scripts/jira/create-jira-traceability.ts +++ b/.github/scripts/jira/create-jira-traceability.ts @@ -45,18 +45,53 @@ async function checkAndAddArtifactPayloadComment( artifactUrl: string ) { const maxResults = 5000; - const comments = await client.issueComments.getComments({ + const getCommentsResponse = await client.issueComments.getComments({ issueIdOrKey: issue, maxResults, // this is the default maxResults, see https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-get }); - core.debug(JSON.stringify(comments.comments)); - if ((comments.total ?? 0) > maxResults) { + core.debug(JSON.stringify(getCommentsResponse.comments)); + if ((getCommentsResponse.total ?? 0) > maxResults) { throw Error( - `Too many (${comments.total}) comments on issue ${issue}, please increase maxResults (${maxResults})` + `Too many (${getCommentsResponse.total}) comments on issue ${issue}, please increase maxResults (${maxResults})` ); } - const commentExists = comments.comments?.some((c) => - c?.body?.text?.includes(artifactUrl) + + // Search path is getCommentsResponse.comments[].body.content[].content[].marks[].attrs.href + // + // Example: + // [ // getCommentsResponse.comments + // { + // body: { + // type: "doc", + // version: 1, + // content: [ + // { + // type: "paragraph", + // content: [ + // { + // type: "text", + // text: "Artifact URL", + // marks: [ + // { + // type: "link", + // attrs: { + // href: "https://github.com/smartcontractkit/chainlink/actions/runs/10517121836/artifacts/1844867108", + // }, + // }, + // ], + // }, + // ], + // }, + // ], + // }, + // }, + // ]; + const commentExists = getCommentsResponse.comments?.some((c) => + c?.body?.content?.some((innerContent) => + innerContent?.content?.some((c) => + c.marks?.some((m) => m.attrs?.href === artifactUrl) + ) + ) ); if (commentExists) { @@ -65,7 +100,29 @@ async function checkAndAddArtifactPayloadComment( core.info(`Adding artifact payload as comment on issue ${issue}`); await client.issueComments.addComment({ issueIdOrKey: issue, - comment: artifactUrl, + comment: { + type: "doc", + version: 1, + content: [ + { + type: "paragraph", + content: [ + { + type: "text", + text: "Artifact Download URL", + marks: [ + { + type: "link", + attrs: { + href: artifactUrl, + }, + }, + ], + }, + ], + }, + ], + }, }); } } From 7ebea7a44fbcf9dc8ede26c892f238d9bc383c7c Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 22 Aug 2024 18:24:16 -0700 Subject: [PATCH 17/32] Use plain artifacts URL for action --- .github/workflows/solidity-foundry-artifacts.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/solidity-foundry-artifacts.yml b/.github/workflows/solidity-foundry-artifacts.yml index 43b71f500e5..e0a3660c3ed 100644 --- a/.github/workflows/solidity-foundry-artifacts.yml +++ b/.github/workflows/solidity-foundry-artifacts.yml @@ -380,8 +380,8 @@ jobs: echo "Base Ref used: **${{ inputs.base_ref }}**" >> $GITHUB_STEP_SUMMARY echo "Commit SHA used: **${{ inputs.commit_to_use || github.sha }}**" >> $GITHUB_STEP_SUMMARY - artifact_url="[Artifact URL](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts/$ARTIFACT_ID)" - echo "$artifact_url" >> $GITHUB_STEP_SUMMARY + artifact_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts/$ARTIFACT_ID" + echo "[Artifact URL]($artifact_url)" >> $GITHUB_STEP_SUMMARY echo "artifact-url=$artifact_url" >> $GITHUB_OUTPUT - name: Setup NodeJS From 3142af84cf3b2aa87bf29782e297b21f4d162741 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 22 Aug 2024 18:24:58 -0700 Subject: [PATCH 18/32] Add test for jira issues in the middle of comments --- .github/scripts/jira/lib.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/scripts/jira/lib.test.ts b/.github/scripts/jira/lib.test.ts index e7d5c00818e..bd7f2000fe7 100644 --- a/.github/scripts/jira/lib.test.ts +++ b/.github/scripts/jira/lib.test.ts @@ -34,6 +34,18 @@ CORE-1011`, const result = parseIssueNumberFrom("No issue number"); expect(result).to.be.undefined; }); + + it("works when the label is in the middle of the commit message", () => { + let r = parseIssueNumberFrom( + "This is a commit message with CORE-123 in the middle", + "CORE-456", + "CORE-789" + ); + expect(r).to.equal("CORE-123"); + + r = parseIssueNumberFrom("#internal address security vulnerabilities RE-2917 around updating nodes and node operators on capabilities registry") + expect(r).to.equal("RE-2917"); + }); }); describe("tagsToLabels", () => { From de5bb6b316cabfd2fd4069e5284b31bb1af5f0f6 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 22 Aug 2024 18:25:10 -0700 Subject: [PATCH 19/32] Formatting --- .github/scripts/jira/lib.test.ts | 68 ++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/.github/scripts/jira/lib.test.ts b/.github/scripts/jira/lib.test.ts index bd7f2000fe7..0b01b12ad4b 100644 --- a/.github/scripts/jira/lib.test.ts +++ b/.github/scripts/jira/lib.test.ts @@ -1,5 +1,11 @@ import { expect, describe, it, vi } from "vitest"; -import { generateIssueLabel, generateJiraIssuesLink, getGitTopLevel, parseIssueNumberFrom, tagsToLabels } from "./lib"; +import { + generateIssueLabel, + generateJiraIssuesLink, + getGitTopLevel, + parseIssueNumberFrom, + tagsToLabels, +} from "./lib"; import * as core from "@actions/core"; describe("parseIssueNumberFrom", () => { @@ -59,10 +65,10 @@ describe("tagsToLabels", () => { }); }); - const mockExecPromise = vi.fn() - vi.mock("util", () => ({ - promisify: () => mockExecPromise, - })); +const mockExecPromise = vi.fn(); +vi.mock("util", () => ({ + promisify: () => mockExecPromise, +})); describe("getGitTopLevel", () => { it("should log the top-level directory when git command succeeds", async () => { @@ -74,8 +80,12 @@ describe("getGitTopLevel", () => { const mockConsoleLog = vi.spyOn(core, "info"); await getGitTopLevel(); - expect(mockExecPromise).toHaveBeenCalledWith("git rev-parse --show-toplevel"); - expect(mockConsoleLog).toHaveBeenCalledWith("Top-level directory: /path/to/top-level-dir"); + expect(mockExecPromise).toHaveBeenCalledWith( + "git rev-parse --show-toplevel" + ); + expect(mockConsoleLog).toHaveBeenCalledWith( + "Top-level directory: /path/to/top-level-dir" + ); }); it("should log an error message when git command fails", async () => { @@ -86,8 +96,12 @@ describe("getGitTopLevel", () => { const mockConsoleError = vi.spyOn(core, "error"); await getGitTopLevel().catch(() => {}); - expect(mockExecPromise).toHaveBeenCalledWith("git rev-parse --show-toplevel"); - expect(mockConsoleError).toHaveBeenCalledWith("Error executing command: Command failed"); + expect(mockExecPromise).toHaveBeenCalledWith( + "git rev-parse --show-toplevel" + ); + expect(mockConsoleError).toHaveBeenCalledWith( + "Error executing command: Command failed" + ); }); it("should log an error message when git command output contains an error", async () => { @@ -99,22 +113,34 @@ describe("getGitTopLevel", () => { const mockConsoleError = vi.spyOn(core, "error"); await getGitTopLevel().catch(() => {}); - expect(mockExecPromise).toHaveBeenCalledWith("git rev-parse --show-toplevel"); - expect(mockConsoleError).toHaveBeenCalledWith("Error in command output: Error: Command failed"); + expect(mockExecPromise).toHaveBeenCalledWith( + "git rev-parse --show-toplevel" + ); + expect(mockConsoleError).toHaveBeenCalledWith( + "Error in command output: Error: Command failed" + ); }); }); -describe('generateJiraIssuesLink', () => { - it('should generate a Jira issues link', () => { - expect(generateJiraIssuesLink("review-artifacts-automation-base:0de9b3b-head:e5b3b9d")).toMatchInlineSnapshot(`"https://smartcontract-it.atlassian.net/issues/?jql=labels+%3D+%22review-artifacts-automation-base%3A0de9b3b-head%3Ae5b3b9d%22"`) +describe("generateJiraIssuesLink", () => { + it("should generate a Jira issues link", () => { + expect( + generateJiraIssuesLink( + "review-artifacts-automation-base:0de9b3b-head:e5b3b9d" + ) + ).toMatchInlineSnapshot( + `"https://smartcontract-it.atlassian.net/issues/?jql=labels+%3D+%22review-artifacts-automation-base%3A0de9b3b-head%3Ae5b3b9d%22"` + ); }); }); -describe('generateIssueLabel', () => { - it('should generate an issue label', () => { - const product = 'automation'; - const baseRef = '0de9b3b'; - const headRef = 'e5b3b9d'; - expect(generateIssueLabel(product, baseRef, headRef)).toMatchInlineSnapshot(`"review-artifacts-automation-base:0de9b3b-head:e5b3b9d"`) +describe("generateIssueLabel", () => { + it("should generate an issue label", () => { + const product = "automation"; + const baseRef = "0de9b3b"; + const headRef = "e5b3b9d"; + expect(generateIssueLabel(product, baseRef, headRef)).toMatchInlineSnapshot( + `"review-artifacts-automation-base:0de9b3b-head:e5b3b9d"` + ); }); -}) +}); From 421da71887e6c5850b8fbf695f39cfba0d3d2b57 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 22 Aug 2024 19:15:45 -0700 Subject: [PATCH 20/32] Actually write to step summary --- .github/scripts/jira/create-jira-traceability.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/scripts/jira/create-jira-traceability.ts b/.github/scripts/jira/create-jira-traceability.ts index 3f1cca7d812..9552aebeaeb 100644 --- a/.github/scripts/jira/create-jira-traceability.ts +++ b/.github/scripts/jira/create-jira-traceability.ts @@ -190,5 +190,6 @@ async function main() { ); core.summary.addLink("Jira Issues", generateJiraIssuesLink(label)); + core.summary.write() } main(); From 78ad24822bee87c56edef05e35e0a9eaf4b21205 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 22 Aug 2024 19:22:53 -0700 Subject: [PATCH 21/32] Make jira host public --- .github/workflows/solidity-foundry-artifacts.yml | 2 +- .github/workflows/solidity-jira.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/solidity-foundry-artifacts.yml b/.github/workflows/solidity-foundry-artifacts.yml index e0a3660c3ed..a41198cc4bf 100644 --- a/.github/workflows/solidity-foundry-artifacts.yml +++ b/.github/workflows/solidity-foundry-artifacts.yml @@ -415,7 +415,7 @@ jobs: HEAD_REF: ${{ inputs.commit_to_use || github.sha }} ARTIFACT_URL: ${{ steps.gather-all-artifacts.outputs.artifact-url }} - JIRA_HOST: ${{ secrets.JIRA_HOST }} + JIRA_HOST: https://smartcontract-it.atlassian.net/ JIRA_USERNAME: ${{ secrets.JIRA_USERNAME }} JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} diff --git a/.github/workflows/solidity-jira.yml b/.github/workflows/solidity-jira.yml index eb3ee35a6f5..f2d8eabdf00 100644 --- a/.github/workflows/solidity-jira.yml +++ b/.github/workflows/solidity-jira.yml @@ -106,7 +106,7 @@ jobs: PR_TITLE: ${{ github.event.pull_request.title }} BRANCH_NAME: ${{ github.event.pull_request.head.ref }} - JIRA_HOST: ${{ secrets.JIRA_HOST }} + JIRA_HOST: https://smartcontract-it.atlassian.net/ JIRA_USERNAME: ${{ secrets.JIRA_USERNAME }} JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} From 0e7d6d70865c5caef8ddbc935403971ef9d5fb61 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 22 Aug 2024 19:57:27 -0700 Subject: [PATCH 22/32] Handle csv output rather than JSON --- .../scripts/jira/create-jira-traceability.ts | 42 +++++++++++-------- .../workflows/solidity-foundry-artifacts.yml | 19 ++------- 2 files changed, 28 insertions(+), 33 deletions(-) diff --git a/.github/scripts/jira/create-jira-traceability.ts b/.github/scripts/jira/create-jira-traceability.ts index 9552aebeaeb..0da6a75c02d 100644 --- a/.github/scripts/jira/create-jira-traceability.ts +++ b/.github/scripts/jira/create-jira-traceability.ts @@ -7,6 +7,30 @@ import { } from "./lib"; import * as core from "@actions/core"; +/** + * Extracts the list of changeset files. Intended to be used with https://github.com/dorny/paths-filter with + * the 'csv' output format. + * + * @returns An array of strings representing the changeset files. + * @throws {Error} If the required environment variable CHANGESET_FILES is missing. + * @throws {Error} If no changeset file exists. + */ +function extractChangesetFiles(): string[] { + const changesetFiles = process.env.CHANGESET_FILES; + if (!changesetFiles) { + throw Error("Missing required environment variable CHANGESET_FILES"); + } + const parsedChangesetFiles = changesetFiles.split(","); + if (parsedChangesetFiles.length === 0) { + throw Error("At least one changeset file must exist"); + } + + core.info( + `Changeset to extract issues from: ${parsedChangesetFiles.join(", ")}` + ); + return parsedChangesetFiles; +} + /** * Adds traceability to JIRA issues by commenting on each issue with a link to the artifact payload * along with a label to connect all issues to the same chainlink product review. @@ -148,22 +172,6 @@ function fetchEnvironmentVariables() { return { product, baseRef, headRef, artifactUrl }; } -function extractChangesetFiles(): string[] { - const changesetFiles = process.env.CHANGESET_FILES; - if (!changesetFiles) { - throw Error("Missing required environment variable CHANGESET_FILES"); - } - const parsedChangesetFiles = JSON.parse(changesetFiles); - if (parsedChangesetFiles.length === 0) { - throw Error("At least one changeset file must exist"); - } - - core.info( - `Changeset to extract issues from: ${parsedChangesetFiles.join(", ")}` - ); - return parsedChangesetFiles; -} - /** * For all affected jira issues listed within the changeset files supplied, * we update each jira issue so that they are all labelled and have a comment linking them @@ -190,6 +198,6 @@ async function main() { ); core.summary.addLink("Jira Issues", generateJiraIssuesLink(label)); - core.summary.write() + core.summary.write(); } main(); diff --git a/.github/workflows/solidity-foundry-artifacts.yml b/.github/workflows/solidity-foundry-artifacts.yml index a41198cc4bf..cb08f2add10 100644 --- a/.github/workflows/solidity-foundry-artifacts.yml +++ b/.github/workflows/solidity-foundry-artifacts.yml @@ -391,31 +391,18 @@ jobs: working-directory: ./.github/scripts/jira run: pnpm i - # For testing, since we need JSON output not CSV - - name: Find modified contracts - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 - id: changes - with: - list-files: 'json' - base: ${{ inputs.base_ref }} - predicate-quantifier: every - filters: | - changeset: - - modified|added: 'contracts/.changeset/!(README)*.md' - - # Because of our earlier checks, we know that both the source and changeset files have changed - name: Create Traceability working-directory: ./.github/scripts/jira run: | pnpm issue:traceability env: - CHANGESET_FILES: ${{ steps.changes.outputs.changeset_files }} - CHAINLINK_PRODUCT: ${{ inputs.product }} + CHANGESET_FILES: ${{ needs.changes.outputs.changeset_files }} + CHAINLINK_PRODUCT: ${{ inputs.product }} BASE_REF: ${{ inputs.base_ref }} HEAD_REF: ${{ inputs.commit_to_use || github.sha }} ARTIFACT_URL: ${{ steps.gather-all-artifacts.outputs.artifact-url }} - JIRA_HOST: https://smartcontract-it.atlassian.net/ + JIRA_HOST: https://smartcontract-it.atlassian.net/ JIRA_USERNAME: ${{ secrets.JIRA_USERNAME }} JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} From a13ad1e0849f0a3c0b60a433f867909be329d28d Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 22 Aug 2024 20:09:55 -0700 Subject: [PATCH 23/32] Fix typo in changeset reference --- .github/workflows/solidity-foundry-artifacts.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/solidity-foundry-artifacts.yml b/.github/workflows/solidity-foundry-artifacts.yml index cb08f2add10..1e5a34836c3 100644 --- a/.github/workflows/solidity-foundry-artifacts.yml +++ b/.github/workflows/solidity-foundry-artifacts.yml @@ -36,8 +36,8 @@ jobs: outputs: product_changes: ${{ steps.changes-transform.outputs.product_changes }} product_files: ${{ steps.changes-transform.outputs.product_files }} - changeset_changes: ${{ steps.changes-dorny.outputs.changeset }} - changeset_files: ${{ steps.changes-dorny.outputs.changeset_files }} + changeset_changes: ${{ steps.changes.outputs.changeset }} + changeset_files: ${{ steps.changes.outputs.changeset_files }} steps: - name: Checkout the repo uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 From 99613b30e4cf87474bcb541a238271c8be1e1e09 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 22 Aug 2024 20:15:22 -0700 Subject: [PATCH 24/32] Comment out broken step --- .github/workflows/solidity-foundry-artifacts.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/solidity-foundry-artifacts.yml b/.github/workflows/solidity-foundry-artifacts.yml index 1e5a34836c3..f7f97576d72 100644 --- a/.github/workflows/solidity-foundry-artifacts.yml +++ b/.github/workflows/solidity-foundry-artifacts.yml @@ -129,7 +129,6 @@ jobs: with: working-directory: contracts - # THIS IS BROKEN - name: Copy modified changesets if: ${{ needs.changes.outputs.changeset_changes == 'true' }} run: | From f24703af969161e904f9d169e12026de80d47e48 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 22 Aug 2024 20:33:07 -0700 Subject: [PATCH 25/32] Notify that auto commits are made via bot --- .github/workflows/solidity-jira.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/solidity-jira.yml b/.github/workflows/solidity-jira.yml index f2d8eabdf00..e9af513bc07 100644 --- a/.github/workflows/solidity-jira.yml +++ b/.github/workflows/solidity-jira.yml @@ -115,7 +115,7 @@ jobs: # Commit appended changeset file back to repo - uses: planetscale/ghcommit-action@v0.1.6 with: - commit_message: "Update changeset file with jira issue" + commit_message: "[Bot] Update changeset file with jira issue" repo: ${{ github.repository }} branch: ${{ github.head_ref }} file_pattern: "contracts/.changeset/*" From 8e9746e3ba7f566eed79746073273202f28a9588 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 22 Aug 2024 20:35:06 -0700 Subject: [PATCH 26/32] Pin planetscale/ghcommit-action --- .github/workflows/solidity-jira.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/solidity-jira.yml b/.github/workflows/solidity-jira.yml index e9af513bc07..2a823cca73b 100644 --- a/.github/workflows/solidity-jira.yml +++ b/.github/workflows/solidity-jira.yml @@ -113,7 +113,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Commit appended changeset file back to repo - - uses: planetscale/ghcommit-action@v0.1.6 + - uses: planetscale/ghcommit-action@13a844326508cdefc72235201bb0446d6d10a85f # v0.1.6 with: commit_message: "[Bot] Update changeset file with jira issue" repo: ${{ github.repository }} From 09fae75c0ce8e3e549412083c499cf951a793c60 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Thu, 22 Aug 2024 20:36:15 -0700 Subject: [PATCH 27/32] Rename --- .github/workflows/{solidity-jira.yml => solidity-tracability.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{solidity-jira.yml => solidity-tracability.yml} (100%) diff --git a/.github/workflows/solidity-jira.yml b/.github/workflows/solidity-tracability.yml similarity index 100% rename from .github/workflows/solidity-jira.yml rename to .github/workflows/solidity-tracability.yml From 302db73a66cc94108159f9debb5860a02b2e58b8 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Sat, 24 Aug 2024 18:43:14 -0700 Subject: [PATCH 28/32] Add pull-requests write perm for comments --- .github/workflows/solidity-tracability.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/solidity-tracability.yml b/.github/workflows/solidity-tracability.yml index 2a823cca73b..9fa1b2b8f93 100644 --- a/.github/workflows/solidity-tracability.yml +++ b/.github/workflows/solidity-tracability.yml @@ -50,6 +50,7 @@ jobs: actions: read id-token: write contents: read + pull-requests: write steps: # https://github.com/planetscale/ghcommit-action/blob/c7915d6c18d5ce4eb42b0eff3f10a29fe0766e4c/README.md?plain=1#L41 # From 74edbeb2619420d2a40acf5c5cbf72d785609650 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Mon, 26 Aug 2024 16:42:17 -0700 Subject: [PATCH 29/32] Add always() to gha metrics --- .github/workflows/solidity-tracability.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/solidity-tracability.yml b/.github/workflows/solidity-tracability.yml index 9fa1b2b8f93..4acf94688a1 100644 --- a/.github/workflows/solidity-tracability.yml +++ b/.github/workflows/solidity-tracability.yml @@ -125,6 +125,7 @@ jobs: - name: Collect Metrics id: collect-gha-metrics + if: always() uses: smartcontractkit/push-gha-metrics-action@d9da21a2747016b3e13de58c7d4115a3d5c97935 # v3.0.1 with: id: soldity-traceability From fb4140c6e7b5d731486e0e08323eb8fb6e84a613 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Mon, 26 Aug 2024 16:48:28 -0700 Subject: [PATCH 30/32] Use env var for head_ref --- .../workflows/solidity-foundry-artifacts.yml | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/.github/workflows/solidity-foundry-artifacts.yml b/.github/workflows/solidity-foundry-artifacts.yml index f7f97576d72..5f03acbe1d6 100644 --- a/.github/workflows/solidity-foundry-artifacts.yml +++ b/.github/workflows/solidity-foundry-artifacts.yml @@ -28,6 +28,9 @@ on: env: FOUNDRY_PROFILE: ci + # Unfortunately, we can't use the "default" field in the inputs section, because it does not have + # access to the workflow context + head_ref: ${{ inputs.commit_to_use || github.sha }} jobs: changes: @@ -42,7 +45,7 @@ jobs: - name: Checkout the repo uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 with: - ref: ${{ inputs.commit_to_use || github.sha }} + ref: ${{ env.head_ref }} - name: Find modified contracts uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 id: changes-dorny @@ -144,13 +147,13 @@ jobs: shell: bash run: | echo "Product: ${{ inputs.product }}" > contracts/commit_sha_base_ref.txt - echo "Commit SHA used to generate artifacts: ${{ inputs.commit_to_use || github.sha }}" >> contracts/commit_sha_base_ref.txt + echo "Commit SHA used to generate artifacts: ${{ env.head_ref }}" >> contracts/commit_sha_base_ref.txt echo "Base reference SHA used to find modified contracts: ${{ inputs.base_ref }}" >> contracts/commit_sha_base_ref.txt IFS=',' read -r -a modified_files <<< "${{ needs.changes.outputs.product_files }}" echo "# Modified contracts:" > contracts/modified_contracts.md for file in "${modified_files[@]}"; do - echo " - [$file](${{ github.server_url }}/${{ github.repository }}/blob/${{ inputs.commit_to_use || github.sha }}/$file)" >> contracts/modified_contracts.md + echo " - [$file](${{ github.server_url }}/${{ github.repository }}/blob/${{ env.head_ref }}/$file)" >> contracts/modified_contracts.md echo "$file" >> contracts/modified_contracts.txt done @@ -187,7 +190,7 @@ jobs: - name: Checkout the repo uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 with: - ref: ${{ inputs.commit_to_use || github.sha }} + ref: ${{ env.head_ref }} - name: Setup NodeJS uses: ./.github/actions/setup-nodejs @@ -268,7 +271,7 @@ jobs: uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 with: fetch-depth: 0 - ref: ${{ inputs.commit_to_use || github.sha }} + ref: ${{ env.head_ref }} - name: Setup NodeJS uses: ./.github/actions/setup-nodejs @@ -315,7 +318,7 @@ jobs: cp contracts/foundry.toml foundry.toml echo "::debug::Processing contracts: $contract_list" - ./contracts/scripts/ci/generate_slither_report.sh "${{ github.server_url }}/${{ github.repository }}/blob/${{ inputs.commit_to_use || github.sha }}/" contracts/configs/slither/.slither.config-artifacts.json "." "$contract_list" "contracts/slither-reports" "--solc-remaps @=contracts/node_modules/@" + ./contracts/scripts/ci/generate_slither_report.sh "${{ github.server_url }}/${{ github.repository }}/blob/${{ env.head_ref }}/" contracts/configs/slither/.slither.config-artifacts.json "." "$contract_list" "contracts/slither-reports" "--solc-remaps @=contracts/node_modules/@" - name: Upload UMLs and Slither reports uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 @@ -346,7 +349,7 @@ jobs: - name: Checkout the repo uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 with: - ref: ${{ inputs.commit_to_use || github.sha }} + ref: ${{ env.head_ref }} - name: Download all artifacts uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 @@ -357,7 +360,7 @@ jobs: - name: Upload all artifacts as single package uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 with: - name: review-artifacts-${{ inputs.product }}-${{ inputs.base_ref }}-${{ inputs.commit_to_use || github.sha }} + name: review-artifacts-${{ inputs.product }}-${{ inputs.base_ref }}-${{ env.head_ref }} path: review_artifacts - name: Remove temporary artifacts @@ -371,13 +374,13 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | ARTIFACTS=$(gh api -X GET repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts) - ARTIFACT_ID=$(echo "$ARTIFACTS" | jq '.artifacts[] | select(.name=="review-artifacts-${{ inputs.product }}-${{ inputs.commit_to_use || github.sha }}") | .id') + ARTIFACT_ID=$(echo "$ARTIFACTS" | jq '.artifacts[] | select(.name=="review-artifacts-${{ inputs.product }}-${{ env.head_ref }}") | .id') echo "Artifact ID: $ARTIFACT_ID" echo "# Solidity Review Artifact Generated" >> $GITHUB_STEP_SUMMARY echo "Product: **${{ inputs.product }}**" >> $GITHUB_STEP_SUMMARY echo "Base Ref used: **${{ inputs.base_ref }}**" >> $GITHUB_STEP_SUMMARY - echo "Commit SHA used: **${{ inputs.commit_to_use || github.sha }}**" >> $GITHUB_STEP_SUMMARY + echo "Commit SHA used: **${{ env.head_ref }}**" >> $GITHUB_STEP_SUMMARY artifact_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts/$ARTIFACT_ID" echo "[Artifact URL]($artifact_url)" >> $GITHUB_STEP_SUMMARY @@ -398,7 +401,7 @@ jobs: CHANGESET_FILES: ${{ needs.changes.outputs.changeset_files }} CHAINLINK_PRODUCT: ${{ inputs.product }} BASE_REF: ${{ inputs.base_ref }} - HEAD_REF: ${{ inputs.commit_to_use || github.sha }} + HEAD_REF: ${{ env.head_ref }} ARTIFACT_URL: ${{ steps.gather-all-artifacts.outputs.artifact-url }} JIRA_HOST: https://smartcontract-it.atlassian.net/ @@ -417,9 +420,9 @@ jobs: run: | echo "# Solidity Review Artifact NOT Generated" >> $GITHUB_STEP_SUMMARY echo "Base Ref used: **${{ inputs.base_ref }}**" >> $GITHUB_STEP_SUMMARY - echo "Commit SHA used: **${{ inputs.commit_to_use || github.sha }}**" >> $GITHUB_STEP_SUMMARY + echo "Commit SHA used: **${{ env.head_ref }}**" >> $GITHUB_STEP_SUMMARY echo "## Reason: No modified Solidity files found for ${{ inputs.product }}" >> $GITHUB_STEP_SUMMARY - echo "* no modified Solidity files found between ${{ inputs.base_ref }} and ${{ inputs.commit_to_use || github.sha }} commits" >> $GITHUB_STEP_SUMMARY + echo "* no modified Solidity files found between ${{ inputs.base_ref }} and ${{ env.head_ref }} commits" >> $GITHUB_STEP_SUMMARY echo "* or they are located outside of ./contracts/src/v0.8 folder" >> $GITHUB_STEP_SUMMARY echo "* or they were limited to test files" >> $GITHUB_STEP_SUMMARY exit 1 From 50869147e30d4affb60ad91571b4f2228a37b6f9 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Mon, 26 Aug 2024 16:56:23 -0700 Subject: [PATCH 31/32] Formatting --- .github/scripts/jira/lib.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/scripts/jira/lib.test.ts b/.github/scripts/jira/lib.test.ts index 0b01b12ad4b..8b3fc864b06 100644 --- a/.github/scripts/jira/lib.test.ts +++ b/.github/scripts/jira/lib.test.ts @@ -49,7 +49,9 @@ CORE-1011`, ); expect(r).to.equal("CORE-123"); - r = parseIssueNumberFrom("#internal address security vulnerabilities RE-2917 around updating nodes and node operators on capabilities registry") + r = parseIssueNumberFrom( + "#internal address security vulnerabilities RE-2917 around updating nodes and node operators on capabilities registry" + ); expect(r).to.equal("RE-2917"); }); }); From 5034911dc9fe3fe1ff70f69d6e927e9430045572 Mon Sep 17 00:00:00 2001 From: HenryNguyen5 <6404866+HenryNguyen5@users.noreply.github.com> Date: Mon, 26 Aug 2024 16:56:47 -0700 Subject: [PATCH 32/32] Use JIRA_HOST rather than hard coded URL --- .github/scripts/jira/create-jira-traceability.ts | 4 +++- .github/scripts/jira/lib.test.ts | 1 + .github/scripts/jira/lib.ts | 10 +++++++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/scripts/jira/create-jira-traceability.ts b/.github/scripts/jira/create-jira-traceability.ts index 0da6a75c02d..b151c9d5eab 100644 --- a/.github/scripts/jira/create-jira-traceability.ts +++ b/.github/scripts/jira/create-jira-traceability.ts @@ -4,6 +4,7 @@ import { extractJiraIssueNumbersFrom, generateIssueLabel, generateJiraIssuesLink, + getJiraEnvVars, } from "./lib"; import * as core from "@actions/core"; @@ -197,7 +198,8 @@ async function main() { artifactUrl ); - core.summary.addLink("Jira Issues", generateJiraIssuesLink(label)); + const { jiraHost } = getJiraEnvVars() + core.summary.addLink("Jira Issues", generateJiraIssuesLink(`${jiraHost}/issues/`, label)); core.summary.write(); } main(); diff --git a/.github/scripts/jira/lib.test.ts b/.github/scripts/jira/lib.test.ts index 8b3fc864b06..6ef629a53ed 100644 --- a/.github/scripts/jira/lib.test.ts +++ b/.github/scripts/jira/lib.test.ts @@ -128,6 +128,7 @@ describe("generateJiraIssuesLink", () => { it("should generate a Jira issues link", () => { expect( generateJiraIssuesLink( + "https://smartcontract-it.atlassian.net/issues/", "review-artifacts-automation-base:0de9b3b-head:e5b3b9d" ) ).toMatchInlineSnapshot( diff --git a/.github/scripts/jira/lib.ts b/.github/scripts/jira/lib.ts index 61a48a892a5..0d0983f5c3e 100644 --- a/.github/scripts/jira/lib.ts +++ b/.github/scripts/jira/lib.ts @@ -5,9 +5,8 @@ import { exec } from "child_process"; import { promisify } from "util"; import { join } from "path"; -export function generateJiraIssuesLink(label: string) { +export function generateJiraIssuesLink(baseUrl: string, label: string) { // https://smartcontract-it.atlassian.net/issues/?jql=labels%20%3D%20%22review-artifacts-automation-base%3A8d818ea265ff08887e61ace4f83364a3ee149ef0-head%3A3c45b71f3610de28f429cef0163936eaa448e63c%22 - const baseUrl = "https://smartcontract-it.atlassian.net/issues/"; const jqlQuery = `labels = "${label}"`; const fullUrl = new URL(baseUrl); fullUrl.searchParams.set("jql", jqlQuery); @@ -103,7 +102,7 @@ export function tagsToLabels(tags: string[]) { })); } -export function createJiraClient() { +export function getJiraEnvVars() { const jiraHost = process.env.JIRA_HOST; const jiraUserName = process.env.JIRA_USERNAME; const jiraApiToken = process.env.JIRA_API_TOKEN; @@ -115,6 +114,11 @@ export function createJiraClient() { process.exit(1); } + return { jiraHost, jiraUserName, jiraApiToken }; +} + +export function createJiraClient() { + const { jiraHost, jiraUserName, jiraApiToken } = getJiraEnvVars(); return new jira.Version3Client({ host: jiraHost, authentication: {