actions / github-script

Write workflows scripting the GitHub API in JavaScript
MIT License
4.21k stars 421 forks source link

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

Closed rdhar closed 8 months ago

rdhar commented 11 months ago

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 ```yml 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 ```yml script: | require(process.env.GITHUB_ACTION_PATH + '/comment.js'); ``` ```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.
kilianc commented 11 months ago

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 })
yhakbar commented 8 months ago

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.

rdhar commented 8 months ago

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 commented 8 months ago

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?