-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
RE-2859 Make jira ticket linkage obligatory in PRs with solidity chan…
…ges (#14054) * Split out lib functions into their own file * Create issue enforcement option * Setup solidity-jira workflow * chore: Improve error message for JIRA issue key not found
- Loading branch information
1 parent
08638ff
commit ebd45ce
Showing
7 changed files
with
257 additions
and
61 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import * as core from "@actions/core"; | ||
import jira from "jira.js"; | ||
import { createJiraClient, parseIssueNumberFrom } from "./lib"; | ||
|
||
async function doesIssueExist( | ||
client: jira.Version3Client, | ||
issueNumber: string, | ||
dryRun: boolean | ||
) { | ||
const payload = { | ||
issueIdOrKey: issueNumber, | ||
}; | ||
|
||
if (dryRun) { | ||
core.info("Dry run enabled, skipping JIRA issue enforcement"); | ||
return true; | ||
} | ||
|
||
try { | ||
/** | ||
* The issue is identified by its ID or key, however, if the identifier doesn't match an issue, a case-insensitive search and check for moved issues is performed. | ||
* If a matching issue is found its details are returned, a 302 or other redirect is not returned. The issue key returned in the response is the key of the issue found. | ||
*/ | ||
const issue = await client.issues.getIssue(payload); | ||
core.debug( | ||
`JIRA issue id:${issue.id} key: ${issue.key} found while querying for ${issueNumber}` | ||
); | ||
if (issue.key !== issueNumber) { | ||
core.error( | ||
`JIRA issue key ${issueNumber} not found, but found issue key ${issue.key} instead. This can happen if the identifier doesn't match an issue, in which case a case-insensitive search and check for moved issues is performed. Make sure the issue key is correct.` | ||
); | ||
return false; | ||
} | ||
|
||
return true; | ||
} catch (e) { | ||
core.debug(e as any); | ||
return false; | ||
} | ||
} | ||
|
||
async function main() { | ||
const prTitle = process.env.PR_TITLE; | ||
const commitMessage = process.env.COMMIT_MESSAGE; | ||
const branchName = process.env.BRANCH_NAME; | ||
const dryRun = !!process.env.DRY_RUN; | ||
const client = createJiraClient(); | ||
|
||
// Checks for the Jira issue number and exit if it can't find it | ||
const issueNumber = parseIssueNumberFrom(prTitle, commitMessage, branchName); | ||
if (!issueNumber) { | ||
const msg = | ||
"No JIRA issue number found in PR title, commit message, or branch name. This pull request must be associated with a JIRA issue."; | ||
|
||
core.setFailed(msg); | ||
return; | ||
} | ||
|
||
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.`); | ||
return; | ||
} | ||
} | ||
|
||
async function run() { | ||
try { | ||
await main(); | ||
} catch (error) { | ||
if (error instanceof Error) { | ||
return core.setFailed(error.message); | ||
} | ||
core.setFailed(error as any); | ||
} | ||
} | ||
|
||
run(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
|
||
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. | ||
* | ||
* @example parseIssueNumberFrom("CORE-123", "CORE-456", "CORE-789") => "CORE-123" | ||
* @example parseIssueNumberFrom("2f3df5gf", "chore/test-RE-78-branch", "RE-78 Create new test branches") => "RE-78" | ||
*/ | ||
export function parseIssueNumberFrom( | ||
...inputs: (string | undefined)[] | ||
): string | undefined { | ||
function parse(str?: string) { | ||
const jiraIssueRegex = /[A-Z]{2,}-\d+/; | ||
|
||
return str?.toUpperCase().match(jiraIssueRegex)?.[0]; | ||
} | ||
|
||
core.debug(`Parsing issue number from: ${inputs.join(", ")}`); | ||
const parsed: string[] = inputs.map(parse).filter((x) => x !== undefined); | ||
core.debug(`Found issue number: ${parsed[0]}`); | ||
|
||
return parsed[0]; | ||
} | ||
|
||
/** | ||
* Converts an array of tags to an array of labels. | ||
* | ||
* A label is a string that is formatted as `core-release/{tag}`, with the leading `v` removed from the tag. | ||
* | ||
* @example tagsToLabels(["v1.0.0", "v1.1.0"]) => [{ add: "core-release/1.0.0" }, { add: "core-release/1.1.0" }] | ||
*/ | ||
export function tagsToLabels(tags: string[]) { | ||
const labelPrefix = "core-release"; | ||
|
||
return tags.map((t) => ({ | ||
add: `${labelPrefix}/${t.substring(1)}`, | ||
})); | ||
} | ||
|
||
export function createJiraClient() { | ||
const jiraHost = process.env.JIRA_HOST; | ||
const jiraUserName = process.env.JIRA_USERNAME; | ||
const jiraApiToken = process.env.JIRA_API_TOKEN; | ||
|
||
if (!jiraHost || !jiraUserName || !jiraApiToken) { | ||
core.setFailed( | ||
"Error: Missing required environment variables: JIRA_HOST and JIRA_USERNAME and JIRA_API_TOKEN." | ||
); | ||
process.exit(1); | ||
} | ||
|
||
return new jira.Version3Client({ | ||
host: jiraHost, | ||
authentication: { | ||
basic: { | ||
email: jiraUserName, | ||
apiToken: jiraApiToken, | ||
}, | ||
}, | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
# 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 | ||
|
||
on: | ||
merge_group: | ||
pull_request: | ||
|
||
defaults: | ||
run: | ||
shell: bash | ||
|
||
jobs: | ||
skip-enforce-jira-issue: | ||
name: Should Skip | ||
# We want to skip merge_group events, and any release branches | ||
# 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 }} | ||
runs-on: ubuntu-latest | ||
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 | ||
with: | ||
list-files: "csv" | ||
# This is a valid input, see https://github.com/dorny/paths-filter/pull/226 | ||
predicate-quantifier: "every" | ||
filters: | | ||
only_src_contracts: | ||
- contracts/**/*.sol | ||
- '!contracts/**/*.t.sol' | ||
- 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. | ||
# | ||
# 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' }} | ||
steps: | ||
- name: Checkout the repo | ||
uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 | ||
|
||
- name: Setup NodeJS | ||
uses: ./.github/actions/setup-nodejs | ||
|
||
- name: Setup Jira | ||
working-directory: ./.github/scripts/jira | ||
run: pnpm i | ||
|
||
- name: Enforce Jira Issue | ||
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 }} | ||
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 }} | ||
|
||
- 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: Enforce Jira Issue | ||
continue-on-error: true |