Skip to content

Commit 0dd6fd1

Browse files
authored
Implement GitHub Action Output Handling (#117)
1 parent bcab9c0 commit 0dd6fd1

File tree

15 files changed

+1316
-73
lines changed

15 files changed

+1316
-73
lines changed

.github/workflows/part_report_deps.yml

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,63 @@ jobs:
4242
chmod +x
4343
./mix_dependency_submission_${{ runner.os }}_${{ runner.arch }}
4444
45-
- run: >-
45+
- id: submit
46+
run: >-
4647
./mix_dependency_submission_${{ runner.os }}_${{ runner.arch }}
4748
--install-deps
4849
--ignore test/fixtures
4950
env:
5051
GITHUB_TOKEN: ${{ github.token }}
52+
53+
- name: "Validate Output submission-json-path not empty"
54+
uses: nick-fields/assert-action@aa0067e01f0f6545c31755d6ca128c5a3a14f6bf # v2.0.0
55+
with:
56+
expected: ""
57+
actual: "${{ steps.submit.outputs.submission-json-path }}"
58+
comparison: "notEqual"
59+
60+
- name: "Validate Output submission-json-path file contents valid JSON"
61+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
62+
with:
63+
script: |
64+
const fs = require('fs');
65+
const path = require('path');
66+
const filePath = process.env.SUBMISSION_JSON_PATH;
67+
const fileContents = fs.readFileSync(filePath, 'utf8');
68+
try {
69+
JSON.parse(fileContents);
70+
return true;
71+
} catch (error) {
72+
throw new Error(`Invalid JSON in file: ${filePath}`);
73+
}
74+
env:
75+
SUBMISSION_JSON_PATH: "${{ steps.submit.outputs.submission-json-path }}"
76+
77+
- name: "Validate output snapshot-id not empty"
78+
uses: nick-fields/assert-action@aa0067e01f0f6545c31755d6ca128c5a3a14f6bf # v2.0.0
79+
with:
80+
expected: ""
81+
actual: "${{ steps.submit.outputs.snapshot-id }}"
82+
comparison: "notEqual"
83+
84+
- name: "Validate output snapshot-api-url is correct"
85+
uses: nick-fields/assert-action@aa0067e01f0f6545c31755d6ca128c5a3a14f6bf # v2.0.0
86+
with:
87+
expected: "${{ github.api_url }}/repos/${{ github.repository }}/dependency-graph/snapshots/${{ steps.submit.outputs.snapshot-id }}"
88+
actual: "${{ steps.submit.outputs.snapshot-api-url }}"
89+
comparison: "exact"
90+
91+
- name: "Can call snapshot-api-url"
92+
id: snapshot-api-call
93+
uses: fjogeleit/http-request-action@23ad54bcd1178fcff6a0d17538fa09de3a7f0a4d #v1.16.4
94+
with:
95+
url: "${{ steps.submit.outputs.snapshot-api-url }}"
96+
method: "GET"
97+
bearerToken: "${{ github.token }}"
98+
99+
- name: "Snapshot API call status code is 200"
100+
uses: nick-fields/assert-action@aa0067e01f0f6545c31755d6ca128c5a3a14f6bf # v2.0.0
101+
with:
102+
expected: "200"
103+
actual: "${{ steps.snapshot-api-call.outputs.status }}"
104+
comparison: "exact"

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,11 @@ jobs:
9393

9494
## Outputs
9595

96-
None.
96+
| Name | Description | Example Value |
97+
|------------------------|---------------------------------------------|-------------------------------------------------------------------------------|
98+
| `submission-json-path` | Path to the generated submission JSON file. | `/tmp/submission-213124323.json` |
99+
| `snapshot-id` | ID of the submission. | `1234` |
100+
| `snapshot-api-url` | URL of the submission API. | `https://api.github.com/repos/{owner}/{repo}/dependency-graph/snapshots/1234` |
97101

98102
## OS and Architecture Support
99103

action.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ inputs:
2020
ignore:
2121
required: false
2222
description: "Paths to ignore"
23+
outputs:
24+
submission-json-path:
25+
description: "Path to the generated submission JSON file"
26+
value: "${{ steps.submit.outputs.submission-json-path }}"
27+
snapshot-id:
28+
description: "ID of the submission"
29+
value: "${{ steps.submit.outputs.snapshot-id }}"
30+
snapshot-api-url:
31+
description: "URL of the submission API"
32+
value: "${{ steps.submit.outputs.snapshot-api-url }}"
2333
runs:
2434
using: "composite"
2535
steps:
@@ -47,6 +57,7 @@ runs:
4757
shell: "bash"
4858
working-directory: "${{ runner.temp }}"
4959
- name: "Submit Dependencies"
60+
id: submit
5061
run: >-
5162
${{ runner.temp }}/mix_dependency_submission_${{ runner.os }}_${{ runner.arch }}
5263
--project-path="$PROJECT_PATH"

config/config.exs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Config
2+
3+
log_level =
4+
case config_env() do
5+
:prod -> :info
6+
_env -> :debug
7+
end
8+
9+
config :logger, level: log_level

coveralls.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
{
22
"skip_files": [
3-
"lib/mix_dependency_submission/application.ex",
4-
"lib/mix_dependency_submission/cli.ex",
5-
"lib/mix_dependency_submission/cli/submit.ex"
3+
"lib/mix_dependency_submission/application.ex"
64
]
75
}

lib/mix_dependency_submission/application.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ defmodule MixDependencySubmission.Application do
1313
Mix.Hex.start()
1414

1515
if Burrito.Util.running_standalone?() do
16-
Submit.run(Args.argv())
16+
exit_code = Submit.run(Args.argv())
17+
18+
System.stop(exit_code)
1719
end
1820

1921
Supervisor.start_link([], strategy: :one_for_one)

lib/mix_dependency_submission/cli.ex

Lines changed: 34 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ defmodule MixDependencySubmission.CLI do
55
Used to configure and validate inputs for submitting a dependency snapshot.
66
"""
77

8+
alias MixDependencySubmission.GitHub
9+
810
@app Mix.Project.config()[:app]
911
@description Mix.Project.config()[:description]
1012
@version Mix.Project.config()[:version]
@@ -25,10 +27,20 @@ defmodule MixDependencySubmission.CLI do
2527
2628
"""
2729
@spec parse!([String.t()]) :: Optimus.ParseResult.t()
28-
def parse!(argv) do
29-
cli_definition()
30-
|> Optimus.new!()
31-
|> Optimus.parse!(argv)
30+
case Mix.env() do
31+
:test ->
32+
def parse!(argv) do
33+
cli_definition()
34+
|> Optimus.new!()
35+
|> Optimus.parse!(argv, &raise("Exit: #{&1}"))
36+
end
37+
38+
_other ->
39+
def parse!(argv) do
40+
cli_definition()
41+
|> Optimus.new!()
42+
|> Optimus.parse!(argv)
43+
end
3244
end
3345

3446
@spec cli_definition :: Optimus.spec()
@@ -59,41 +71,41 @@ defmodule MixDependencySubmission.CLI do
5971
value_name: "GITHUB_API_URL",
6072
long: "--github-api-url",
6173
help: "GitHub API URL",
62-
parser: &parse_github_api_url/1,
63-
default: System.get_env("GITHUB_API_URL", "https://api.github.com")
74+
parser: &parse_uri/1,
75+
default: GitHub.get_api_url()
6476
],
6577
github_repository:
66-
optimus_options_with_env_default("GITHUB_REPOSITORY",
78+
optimus_options_with_fun_default(&GitHub.fetch_repository/0,
6779
value_name: "GITHUB_REPOSITORY",
6880
long: "--github-repository",
6981
help: ~S(GitHub repository name "owner/repository")
7082
),
7183
github_job_id:
72-
optimus_options_with_env_default("GITHUB_JOB",
84+
optimus_options_with_fun_default(&GitHub.fetch_job_id/0,
7385
value_name: "GITHUB_JOB",
7486
long: "--github-job-id",
7587
help: "GitHub Actions Job ID"
7688
),
7789
github_workflow:
78-
optimus_options_with_env_default("GITHUB_WORKFLOW",
90+
optimus_options_with_fun_default(&GitHub.fetch_workflow/0,
7991
value_name: "GITHUB_WORKFLOW",
8092
long: "--github-workflow",
8193
help: "GitHub Actions Workflow Name"
8294
),
8395
sha:
84-
sha_option(
96+
optimus_options_with_fun_default(&GitHub.fetch_head_sha/0,
8597
value_name: "SHA",
8698
long: "--sha",
8799
help: "Current Git SHA"
88100
),
89101
ref:
90-
optimus_options_with_env_default("GITHUB_REF",
102+
optimus_options_with_fun_default(&GitHub.fetch_ref/0,
91103
value_name: "REF",
92104
long: "--ref",
93105
help: "Current Git Ref"
94106
),
95107
github_token:
96-
optimus_options_with_env_default("GITHUB_TOKEN",
108+
optimus_options_with_fun_default(&GitHub.fetch_token/0,
97109
value_name: "GITHUB_TOKEN",
98110
long: "--github-token",
99111
help: "GitHub Token"
@@ -126,55 +138,22 @@ defmodule MixDependencySubmission.CLI do
126138
end
127139
end
128140

129-
@spec parse_github_api_url(uri :: String.t()) :: Optimus.parser_result()
130-
defp parse_github_api_url(uri) do
141+
@spec parse_uri(uri :: String.t()) :: Optimus.parser_result()
142+
defp parse_uri(uri) do
131143
with {:ok, %URI{}} <- URI.new(uri) do
132-
uri
144+
{:ok, uri}
133145
end
134146
end
135147

136-
@spec optimus_options_with_env_default(env :: String.t(), details :: Keyword.t()) :: Keyword.t()
137-
defp optimus_options_with_env_default(env, details) do
138-
case System.fetch_env(env) do
148+
@spec optimus_options_with_fun_default(
149+
fetch_fun :: (-> {:ok, value} | :error),
150+
details :: Keyword.t()
151+
) :: Keyword.t()
152+
when value: term()
153+
defp optimus_options_with_fun_default(fetch_fun, details) when is_function(fetch_fun, 0) do
154+
case fetch_fun.() do
139155
{:ok, value} -> [default: value]
140156
:error -> [required: true]
141157
end ++ details
142158
end
143-
144-
@spec sha_option(Keyword.t()) :: Keyword.t()
145-
defp sha_option(base_opts) do
146-
# If the GitHub event is a pull request, we need to use the head SHA of the PR
147-
# instead of the commit SHA of the workflow run.
148-
# This is because the workflow run is triggered by the base commit of the PR,
149-
# and we want to report the dependencies of the head commit.
150-
# See: https://github.com/github/dependency-submission-toolkit/blob/72f5e31325b5e1bcc91f1b12eb7abe68e75b2105/src/snapshot.ts#L36-L61
151-
case load_pr_head_sha() do
152-
{:ok, sha} ->
153-
Keyword.put(base_opts, :default, sha)
154-
155-
:error ->
156-
# If we can't load the PR head SHA, we fall back to the default behavior
157-
# of using the GITHUB_SHA environment variable.
158-
optimus_options_with_env_default("GITHUB_SHA", base_opts)
159-
end
160-
end
161-
162-
# Note that pull_request_target is omitted here.
163-
# That event runs in the context of the base commit of the PR,
164-
# so the snapshot should not be associated with the head commit.
165-
166-
@pr_events ~w[pull_request pull_request_comment pull_request_review pull_request_review_comment]
167-
168-
@spec load_pr_head_sha :: {:ok, <<_::320>>} | :error
169-
defp load_pr_head_sha do
170-
with {:ok, event} when event in @pr_events <- System.fetch_env("GITHUB_EVENT_NAME"),
171-
{:ok, event_path} <- System.fetch_env("GITHUB_EVENT_PATH") do
172-
event_details_json = File.read!(event_path)
173-
174-
%{"pull_request" => %{"head" => %{"sha" => <<_binary::320>> = sha}}} =
175-
JSON.decode!(event_details_json)
176-
177-
{:ok, sha}
178-
end
179-
end
180159
end

lib/mix_dependency_submission/cli/submit.ex

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ defmodule MixDependencySubmission.CLI.Submit do
1010

1111
alias MixDependencySubmission.ApiClient
1212
alias MixDependencySubmission.CLI
13+
alias MixDependencySubmission.GitHub
1314

1415
require Logger
1516

@@ -44,9 +45,11 @@ defmodule MixDependencySubmission.CLI.Submit do
4445
...> "--github-repository",
4546
...> "org/repo"
4647
...> ])
48+
# Exit Code
49+
0
4750
4851
"""
49-
@spec run(argv :: [String.t()]) :: no_return()
52+
@spec run(argv :: [String.t()]) :: non_neg_integer()
5053
def run(argv) do
5154
%Optimus.ParseResult{
5255
options: %{
@@ -78,20 +81,71 @@ defmodule MixDependencySubmission.CLI.Submit do
7881
ignore: ignore
7982
)
8083

81-
Logger.info("Calculated Submission: #{Jason.encode!(submission, pretty: true)}")
84+
submission_json = Jason.encode!(submission, pretty: true)
85+
86+
submission_path =
87+
Path.join(System.tmp_dir!(), "submission-#{:erlang.crc32(submission_json)}.json")
88+
89+
File.write!(submission_path, submission_json)
90+
91+
Logger.info("Calculated Submission, written to: #{submission_path}")
92+
93+
GitHub.write_output("submission-json-path", submission_path)
8294

8395
submission
8496
|> ApiClient.submit(github_api_url, github_repository, github_token)
8597
|> case do
86-
{:ok, %Req.Response{body: body}} ->
87-
Logger.info("Successfully submitted submission")
88-
Logger.debug("Success Response: #{inspect(body, pretty: true)}")
89-
90-
System.halt(0)
98+
{:ok, %Req.Response{body: body, status: 201}} ->
99+
report_result(body, github_api_url, github_repository)
91100

92101
{:error, {:unexpected_response, response}} ->
93102
Logger.error("Unexpected response: #{inspect(response, pretty: true)}")
94-
System.stop(1)
103+
104+
2
95105
end
96106
end
107+
108+
defp report_result(body, github_api_url, github_repository)
109+
110+
defp report_result(
111+
%{"id" => submission_id, "message" => message, "result" => result} = body,
112+
github_api_url,
113+
github_repository
114+
)
115+
when result in ~w[SUCCESS ACCEPTED] do
116+
Logger.info("Successfully submitted submission: #{result}: #{message}")
117+
Logger.debug("Success Response: #{inspect(body, pretty: true)}")
118+
119+
GitHub.write_output("snapshot-id", submission_id)
120+
121+
GitHub.write_output(
122+
"snapshot-api-url",
123+
github_api_url <> "/repos/#{github_repository}/dependency-graph/snapshots/#{submission_id}"
124+
)
125+
126+
0
127+
end
128+
129+
@spec report_result(
130+
body :: %{String.t() => term()},
131+
github_api_url :: String.t(),
132+
github_repository :: String.t()
133+
) :: non_neg_integer()
134+
defp report_result(
135+
%{"id" => submission_id, "message" => message, "result" => "INVALID"} = body,
136+
github_api_url,
137+
github_repository
138+
) do
139+
Logger.error("Invalid submission: #{message}")
140+
Logger.debug("Invalid Response: #{inspect(body, pretty: true)}")
141+
142+
GitHub.write_output("snapshot-id", submission_id)
143+
144+
GitHub.write_output(
145+
"snapshot-api-url",
146+
github_api_url <> "/repos/#{github_repository}/dependency-graph/snapshots/#{submission_id}"
147+
)
148+
149+
1
150+
end
97151
end

0 commit comments

Comments
 (0)