github-script icon indicating copy to clipboard operation
github-script copied to clipboard

How to reference a separate script file which processes and returns output?

Open rdhar opened this issue 2 years ago • 3 comments

Discussed in https://github.com/actions/github-script/discussions/433

Originally posted by rdhar November 13, 2023 Hi, I would like to strip out snippets of JS code into separate files outside of YAML for ease of legibility. However, I'm struggling to understand how best to achieve something this.

Before/Current
script: |
    const { data: list_comments } = await github.rest.issues.listComments({
    issue_number: context.issue.number,
    owner: context.repo.owner,
    per_page: 100,
    repo: context.repo.repo,
  });
  const get_comment = list_comments
    .sort((a, b) => b.id - a.id)
    .find((comment) => /^keyword/.test(comment.body));
  return {
    body: get_comment.body,
    id: get_comment.id,
  };
After/Proposed
script: |
  require(process.env.GITHUB_ACTION_PATH + '/comment.js');
// File: comment.js
const { data: list_comments } = await github.rest.issues.listComments({
  issue_number: context.issue.number,
  owner: context.repo.owner,
  per_page: 100,
  repo: context.repo.repo,
});
const get_comment = list_comments
  .sort((a, b) => b.id - a.id)
  .find((comment) => /^keyword/.test(comment.body));
return {
  body: get_comment.body,
  id: get_comment.id,
};

With this, I get: "SyntaxError: await is only valid in async functions and the top level bodies of modules." If I drop the await, then I get: "ReferenceError: github is not defined."

I'm sure I'm missing something obvious with module.exports = ({ github, context }) => { ... }, but I'm not sure how best to address this particular script which: makes an API call, processes the response, and returns the output in that specific order.

Really appreciate any thoughts/inputs, thanks for your time.

rdhar avatar Nov 17 '23 15:11 rdhar

I would argue that first class external script support is a desirable feature! The default is to support await in code assigned to script, there is an eventual need to move the script into a file, and the likely expectation is to copy paste the content to a file and everything works exactly the same.

The workaround I have for now is:

...
  script: |
    const fn = require('${{ github.workspace }}/.github/fn.js')
    await fn({
      github,
      context,
      core,
      glob,
      io,
      exec,
      require
    })

or

...
  script: await require('${{ github.workspace }}/.github/fn.js')({ github, context, core, glob, io, exec, require })

kilianc avatar Dec 02 '23 04:12 kilianc

In this example, I believe you would do something like the following, per the docs for async here:

script: |
  const script = require(process.env.GITHUB_ACTION_PATH + '/comment.js');
  await script({github, context, core})
// File: comment.js
module.exports = async ({ github, context, core  }) => {
  const { data: list_comments } = await github.rest.issues.listComments({
    issue_number: context.issue.number,
    owner: context.repo.owner,
    per_page: 100,
    repo: context.repo.repo,
  });
  const get_comment = list_comments
    .sort((a, b) => b.id - a.id)
    .find((comment) => /^keyword/.test(comment.body));
  return {
    body: get_comment.body,
    id: get_comment.id,
  };
}

If you have more that you need to do, you can require more scripts and await them after calling the first one, etc.

yhakbar avatar Feb 14 '24 15:02 yhakbar

I would argue that first class external script support is a desirable feature! The default is to support await in code assigned to script, there is an eventual need to move the script into a file, and the likely expectation is to copy paste the content to a file and everything works exactly the same.

Great to hear your support, @kilianc, and agree that first-class support for external script files would be ideal to match parity with run: bash script.sh per GitHub docs.

rdhar avatar Feb 14 '24 15:02 rdhar

In this example, I believe you would do something like the following, per the docs for async here: If you have more that you need to do, you can require more scripts and await them after calling the first one, etc.

Brilliant, thank you for sharing, @yhakbar!

That script setup and module.exports = async... is exactly what was needed -- the only tweak was to replace the return { ... } with core.setOutput(), like so:

// Before
return {
  body: get_comment.body,
  id: get_comment.id,
};

// After
core.setOutput("body", get_comment.body);
core.setOutput("id", get_comment.id);

Similarly, I had to amend references to the output result within the action.yml workflow as well:

# Before
fromJSON(steps.comment.outputs.result)['body']
fromJSON(steps.comment.outputs.result)['id']

# After
steps.comment.outputs.body
steps.comment.outputs.id

To recognize your contribution, would you mind sharing your answer on the original Q&A #433 discussion, where I can accept it as the correct answer?

rdhar avatar Feb 20 '24 17:02 rdhar