Skip to content

Commit

Permalink
Merge branch 'main' into henrymorales/c#
Browse files Browse the repository at this point in the history
  • Loading branch information
hmmorales authored Dec 11, 2024
2 parents e961a8d + faebe07 commit 61e7a8b
Show file tree
Hide file tree
Showing 192 changed files with 13,837 additions and 3,800 deletions.
27 changes: 27 additions & 0 deletions .github/actions/add-label-artifact/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Add Label Artifact
description: Uploads an empty artifact named `label-${name}=${value}`, that's consumed by action "update-labels"

inputs:
name:
description: Name
required: true
value:
description: Value ("true" or "false")
required: true

runs:
using: composite

steps:
- name: Create empty file to upload artifact
run: "> $RUNNER_TEMP/empty.txt"
shell: bash

# The maximum length is reported to be 260 characters. A full list of invalid artifact name characters is documented here:
# https://github.com/actions/toolkit/blob/main/packages/artifact/src/internal/upload/path-and-artifact-name-validation.ts
- uses: actions/upload-artifact@v4
with:
name: label-${{ inputs.name }}=${{ inputs.value }}
path: ${{ runner.temp }}/empty.txt
if-no-files-found: error
overwrite: true
95 changes: 95 additions & 0 deletions .github/actions/context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// @ts-check

/**
* Extracts inputs from context based on event name and properties.
* run_id is only defined for "workflow_run:completed" events.
*
* @param {import('github-script').AsyncFunctionArguments['github']} github
* @param {import('github-script').AsyncFunctionArguments['context']} context
* @param {import('github-script').AsyncFunctionArguments['core']} core
* @returns {Promise<{owner: string, repo: string, head_sha: string, issue_number: number, run_id: number }>}
*/
async function extractInputs(github, context, core) {
core.info(`extractInputs(${context.eventName}, ${context.payload.action})`);

// Add support for more event types as needed
if (context.eventName === "pull_request") {
const payload =
/** @type {import("@octokit/webhooks-types").PullRequestEvent} */ (
context.payload
);

const inputs = {
owner: payload.repository.owner.login,
repo: payload.repository.name,
head_sha: payload.pull_request.head.sha,
issue_number: payload.number,
run_id: NaN
};

core.info(`inputs: ${JSON.stringify(inputs)}`);

return inputs;
} else if (
context.eventName === "workflow_run" &&
context.payload.action === "completed"
) {
const payload =
/** @type {import("@octokit/webhooks-types").WorkflowRunCompletedEvent} */ (
context.payload
);

let issue_number;

const pull_requests = payload.workflow_run.pull_requests;
if (pull_requests && pull_requests.length > 0) {
// For non-fork PRs, we should be able to extract the PR number from the payload, which avoids an
// unnecessary API call. The listPullRequestsAssociatedWithCommit() API also seems to return
// empty for non-fork PRs.
issue_number = pull_requests[0].number;
} else {
// For fork PRs, we must call an API in the head repository to get the PR number in the target repository

// Owner and repo for the PR head (may differ from target for fork PRs)
const head_owner = payload.workflow_run.head_repository.owner.login;
const head_repo = payload.workflow_run.head_repository.name;
const head_sha = payload.workflow_run.head_sha;

core.info(
`listPullRequestsAssociatedWithCommit(${head_owner}, ${head_repo}, ${head_sha})`,
);
const { data: pullRequests } =
await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner: head_owner,
repo: head_repo,
commit_sha: head_sha,
});

if (pullRequests.length === 1) {
issue_number = pullRequests[0].number;
} else {
throw new Error(
`Unexpected number of pull requests associated with commit '${head_sha}'. Expected: '1'. Actual '${pullRequests.length}'.`,
);
}
}

const inputs = {
owner: payload.workflow_run.repository.owner.login,
repo: payload.workflow_run.repository.name,
head_sha: payload.workflow_run.head_sha,
issue_number: issue_number,
run_id: payload.workflow_run.id,
};

core.info(`inputs: ${JSON.stringify(inputs)}`);

return inputs;
} else {
throw new Error(
`Invalid context: '${context.eventName}:${context.payload.action}'. Expected 'workflow_run:completed'.`,
);
}
}

module.exports = { extractInputs };
103 changes: 103 additions & 0 deletions .github/actions/update-labels/action.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// @ts-check

const { extractInputs } = require("../context");

/**
* @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments
*/
module.exports = async ({ github, context, core }) => {
let owner = process.env.OWNER;
let repo = process.env.REPO;
let issue_number = parseInt(process.env.ISSUE_NUMBER || "");
let run_id = parseInt(process.env.RUN_ID || "");

if (!owner || !repo || !(issue_number || run_id)) {
let inputs = await extractInputs(github, context, core);
owner = owner || inputs.owner;
repo = repo || inputs.repo;
issue_number = issue_number || inputs.issue_number;
run_id = run_id || inputs.run_id;
}

/** @type {string[]} */
let artifactNames = [];

if (run_id) {
// List artifacts from a single run_id
core.info(`listWorkflowRunArtifacts(${owner}, ${repo}, ${run_id})`);
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: owner,
repo: repo,
run_id: run_id,
});

artifactNames = artifacts.data.artifacts.map((a) => a.name);
} else {
// TODO: List all artifacts of all workflows associated with issue_number
throw new Error("Required input 'run_id' not found in env or context");
}

core.info(`artifactNames: ${JSON.stringify(artifactNames)}`);

/** @type {string[]} */
const labelsToAdd = [];

/** @type {string[]} */
const labelsToRemove = [];

for (const artifactName of artifactNames) {
// If artifactName has format "label-name=true|false", add or remove the label
// Else, if artifactName has format "label-name=other-string", throw an error
// Else, if artifactName does not start with "label-", ignore it
const firstEquals = artifactName.indexOf("=");
if (firstEquals !== -1) {
const key = artifactName.substring(0, firstEquals);
const value = artifactName.substring(firstEquals + 1);

if (key.startsWith("label-")) {
const name = key.substring("label-".length);
if (value === "true") {
labelsToAdd.push(name);
} else if (value === "false") {
labelsToRemove.push(name);
} else {
throw new Error(
`Invalid value for label '${name}': ${value}. Expected "true" or "false".`,
);
}
}
}
}

core.info(`labelsToAdd: ${JSON.stringify(labelsToAdd)}`);
core.info(`labelsToRemove: ${JSON.stringify(labelsToRemove)}`);

if (labelsToAdd.length > 0) {
await github.rest.issues.addLabels({
owner: owner,
repo: repo,
issue_number: issue_number,
labels: labelsToAdd,
});
}

if (labelsToRemove.length > 0) {
// Must loop over labelsToRemove ourselves, since GitHub doesn't expose a REST API to remove in bulk.
for (const name of labelsToRemove) {
try {
await github.rest.issues.removeLabel({
owner: owner,
repo: repo,
issue_number: issue_number,
name: name,
});
} catch (error) {
if (error.status === 404) {
core.info(`Ignoring error: ${error.status} - ${error.message}`);
} else {
throw error;
}
}
}
}
};
33 changes: 33 additions & 0 deletions .github/actions/update-labels/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Update Labels
description: Adds or removes labels to set state matching artifact names

# If any inputs are not set, we will attempt to extract them from the event context
inputs:
owner:
description: The account owner of the repository. The name is not case sensitive.
required: false
repo:
description: The name of the repository without the .git extension. The name is not case sensitive.
required: false
issue_number:
description: The issue that should have its labels updated.
required: false
run_id:
description: Updates labels from a single completed workflow.
required: false

runs:
using: composite

steps:
- name: Set Label
uses: actions/github-script@v7
env:
OWNER: ${{ inputs.owner }}
REPO: ${{ inputs.repo }}
ISSUE_NUMBER: ${{ inputs.issue_number }}
RUN_ID: ${{ inputs.run_id }}
with:
script: |
const action = require('./.github/actions/update-labels/action.js')
await action({ github, context, core });
Loading

0 comments on commit 61e7a8b

Please sign in to comment.