Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add read-file action #203

Merged
merged 43 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
e124b04
Add read-file
kenodegard Aug 16, 2024
443592e
Use YYYY.MM cache hash
kenodegard Aug 16, 2024
3a314b2
Correct inputs & outputs
kenodegard Aug 16, 2024
7b49fe1
Cleanup
kenodegard Aug 16, 2024
e060186
Add default value for when file doesn't exist
kenodegard Sep 9, 2024
719ff88
Convert positionals to options
kenodegard Sep 9, 2024
9d4f86c
Invalidate cache based on workflows
kenodegard Dec 17, 2024
77bf54a
Merge remote-tracking branch 'upstream/main' into read-file
kenodegard Dec 17, 2024
ee841d4
Correct README
kenodegard Dec 17, 2024
d901854
Improve caching
kenodegard Dec 20, 2024
4ee78f6
Use Python >=3.8
kenodegard Dec 20, 2024
f9ab207
Initialize test suite
kenodegard Jan 8, 2025
132e3cf
Install all dependencies
kenodegard Jan 8, 2025
0d9ef43
Upload coverage
kenodegard Jan 8, 2025
06d63b9
Add badges
kenodegard Jan 8, 2025
ee03d8e
Update README.md
kenodegard Jan 8, 2025
77bfcb1
Update test_combine_durations.py
kenodegard Jan 8, 2025
7a51913
Merge branch 'add-tests' into read-file
kenodegard Jan 8, 2025
5b2f8cc
Add test_read_file.py
kenodegard Jan 8, 2025
ba8da4e
Correct nested double quotes
kenodegard Jan 9, 2025
8f33631
Use Python >=3.9
kenodegard Jan 9, 2025
88fa4be
Stringify
kenodegard Jan 9, 2025
4357eeb
Don't specify testpaths, allow pytest to autodetect
kenodegard Jan 9, 2025
e71f8ec
Merge remote-tracking branch 'upstream/main' into read-file
kenodegard Jan 9, 2025
470feb1
Add comment regarding hashing
kenodegard Jan 9, 2025
eff5b38
Test GHA
kenodegard Jan 9, 2025
3293207
Debug
kenodegard Jan 9, 2025
e177038
Correct remote URL
kenodegard Jan 9, 2025
6fbd352
Fix quoting
kenodegard Jan 9, 2025
f8de1ff
Setup python
kenodegard Jan 9, 2025
bd357d2
Standardize Python
kenodegard Jan 9, 2025
5e0833d
Enable dependabot updates for pip
kenodegard Jan 9, 2025
cacdba9
Add analyze step with automated error reports
kenodegard Jan 9, 2025
e145ba9
Minor cleanup
kenodegard Jan 9, 2025
92e1cce
Update existing failures
kenodegard Jan 9, 2025
1037a60
Merge branch 'scheduled-tests' into read-file
kenodegard Jan 9, 2025
d8091a2
Apply suggestions from code review
kenodegard Jan 9, 2025
feca3bd
Typing
kenodegard Jan 9, 2025
04a54aa
Update README.md
kenodegard Jan 10, 2025
3cb55bf
Apply suggestions from code review
kenodegard Jan 10, 2025
752288a
Update .github/workflows/tests.yml
kenodegard Jan 10, 2025
942bda4
Merge remote-tracking branch 'upstream/main' into read-file
kenodegard Jan 10, 2025
d6226e8
Merge branch 'main' into read-file
jezdez Jan 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/TEST_FAILURE_REPORT_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: '{{ env.TITLE }} ({{ date | date("YYYY-MM-DD") }})'
labels: ['type::bug', 'type::testing', 'source::auto']
---

The {{ workflow }} workflow failed on {{ date | date("YYYY-MM-DD HH:mm") }} UTC

Full run: {{ repo.html_url }}/actions/runs/{{ env.RUN_ID }}
4 changes: 4 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ updates:
directory: /set-commit-status
schedule:
interval: weekly
- package-ecosystem: pip
directory: /
schedule:
interval: weekly
73 changes: 71 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ concurrency:
cancel-in-progress: true

jobs:
tests:
pytest:
runs-on: ubuntu-latest
steps:
- name: Checkout Source
Expand All @@ -34,7 +34,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
with:
python-version: '3.11'
python-version: '>=3.9'

- name: Install Dependencies
run: pip install --quiet -r requirements.txt -r combine-durations/requirements.txt -r template-files/requirements.txt
Expand All @@ -46,3 +46,72 @@ jobs:
uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
with:
token: ${{ secrets.CODECOV_TOKEN }}

read-file:
runs-on: ubuntu-latest
steps:
- name: Write GitHub context to log
env:
GITHUB_CONTEXT: ${{ toJSON(github) }}
run: echo "$GITHUB_CONTEXT"

kenodegard marked this conversation as resolved.
Show resolved Hide resolved
- name: Checkout Source
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Read Remote JSON
id: json
uses: ./read-file
env:
# `full_name` is the org/repo slug, extracting from `head` in case PR was opened via a fork
FULL_NAME: ${{ github.event.pull_request.head.repo.full_name || github.event.repository.full_name }}
# `head_ref` is the pull_request branch
# `ref_name` is the push/workflow_dispatch/schedule branch
REF: ${{ github.head_ref || github.ref_name }}
with:
path: https://raw.githubusercontent.com/${{ env.FULL_NAME }}/refs/heads/${{ env.REF }}/read-file/data/json.json
parser: json

- name: Read Local YAML
id: yaml
uses: ./read-file
with:
path: ./read-file/data/yaml.yaml
parser: yaml

- name: Setup Python
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
with:
python-version: '>=3.9'

- name: Run Tests
shell: python
run: |
assert '''${{ steps.json.outputs.content }}''' == '''${{ steps.yaml.outputs.content }}'''
assert '''${{ fromJSON(steps.json.outputs.content)['foo'] }}''' == '''${{ fromJSON(steps.yaml.outputs.content)['foo'] }}'''
Comment on lines +84 to +85
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to use triple single quotes here and not single double quotes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the json values might contain quotes at some point in the future 🤷🏼‍♂️


# required check
analyze:
needs: [pytest, read-file]
if: '!cancelled()'
runs-on: ubuntu-latest
steps:
- name: Determine Success
uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2
id: alls-green
with:
jobs: ${{ toJSON(needs) }}

- name: Checkout our source
if: always() && github.event_name != 'pull_request' && steps.alls-green.outputs.result == 'failure'
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Report failures
if: always() && github.event_name != 'pull_request' && steps.alls-green.outputs.result == 'failure'
uses: JasonEtco/create-an-issue@1b14a70e4d8dc185e5cc76d3bec9eab20257b2c5 # v2.9.2
env:
GITHUB_TOKEN: ${{ secrets.AUTO_REPORT_TEST_FAILURE }}
RUN_ID: ${{ github.run_id }}
TITLE: 🤖 Tests Failed
with:
filename: .github/TEST_FAILURE_REPORT_TEMPLATE.md
update_existing: true
2 changes: 1 addition & 1 deletion combine-durations/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ runs:

- uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
with:
python-version: '3.11'
python-version: '>=3.9'

- name: Install Dependencies
shell: bash
Expand Down
18 changes: 14 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,22 @@
skip = '.git'

[tool.coverage.report]
exclude_also = ["if TYPE_CHECKING:"]
exclude_also = [
"if TYPE_CHECKING:",
'if __name__ == "__main__":',
]

[tool.coverage.run]
omit = ["**/test_*.py"]
omit = [
"**/test_*.py",
'if __name__ == "__main__":',
]

[tool.pytest.ini_options]
addopts = [
"--color=yes",
"--cov=combine-durations",
"--cov=read-file",
"--cov=template-files",
"--cov-append",
"--cov-branch",
Expand All @@ -21,7 +28,6 @@ addopts = [
"--tb=native",
"-vv",
]
testpaths = ["**/"]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason this doesn't find test_read_file.py but the pytest's default recursive search finds the tests


[tool.ruff]
target-version = "py39"
Expand All @@ -42,4 +48,8 @@ select = [
]

[tool.ruff.lint.isort]
known-first-party = ["combine_durations", "template_files"]
known-first-party = [
"combine_durations",
"read_file",
"template_files",
]
52 changes: 52 additions & 0 deletions read-file/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Read File

A composite GitHub Action to read a local file or remote URL with an optional JSON/YAML parser.

## Action Inputs

| Name | Description | Default |
|------|-------------|---------|
| `path` | Local path or remote URL to the file to read. | **required** |
| `parser` | Parser to use for the file. Choose json, yaml, or null (to leave it as plain text). | **optional** |
| `default` | File contents to use if the file is not found. | **optional** |

## Action Outputs

| Name | Description |
|------|-------------|
| `content` | File contents as a JSON object (if a parser is specified) or the raw text. |

## Sample Workflows

```yaml
name: Read File

on:
pull_request:

jobs:
read:
steps:
- id: read_json
uses: conda/actions/read-file
with:
path: https://raw.githubusercontent.com/owner/repo/ref/path/to/json.json
default: '{}'
parser: json

- id: read_yaml
uses: conda/actions/read-file
with:
path: https://raw.githubusercontent.com/owner/repo/ref/path/to/yaml.yaml
default: '{}'
parser: yaml

- id: read_text
uses: conda/actions/read-file
with:
path: path/to/text.text

- run: echo "${{ fromJSON(steps.read_json.outputs.content)['key'] }}"
- run: echo "${{ fromJSON(steps.read_yaml.outputs.content)['key'] }}"
- run: echo "${{ steps.read_file.outputs.content }}"
```
59 changes: 59 additions & 0 deletions read-file/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Read File
description: Read a local file or remote URL.
author: Anaconda Inc.
branding:
icon: book-open
color: green

inputs:
path:
description: Local path or remote URL to the file to read.
required: true
parser:
description: Parser to use for the file. Choose json, yaml, or null (to leave it as plain text).
default:
description: File contents to use if the file is not found.
outputs:
content:
description: File contents as a JSON object (if a parser is specified) or the raw text.
value: ${{ steps.read.outputs.content }}

runs:
using: composite
steps:
# `hashFiles` only works on files within the working directory, since `requirements.txt`
# is not in the working directory we need to manually compute the SHA256 hash
- name: Compute Hash
id: hash
shell: bash
run: echo hash=$(sha256sum ${{ github.action_path }}/requirements.txt | awk '{print $1}') >> $GITHUB_OUTPUT

- name: Pip Cache
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: ~/.cache/pip
key: ${{ github.workflow }}-read-file-${{ steps.hash.outputs.hash }}

- name: Setup Python
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
with:
python-version: '>=3.9'

- name: Pip Install
shell: bash
run: pip install --quiet -r ${{ github.action_path }}/requirements.txt

- name: Pip List
shell: bash
run: pip list

- name: Read JSON
id: read
shell: bash
run: >
python ${{ github.action_path }}/read_file.py
${{ inputs.path }}
${{ inputs.parser && format('"--parser={0}"', inputs.parser) || '' }}
${{ inputs.default && format('"--default={0}"', inputs.default) || '' }}
env:
GITHUB_TOKEN: ${{ github.token }}
1 change: 1 addition & 0 deletions read-file/data/json.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"foo": "bar"}
1 change: 1 addition & 0 deletions read-file/data/yaml.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
foo: bar
97 changes: 97 additions & 0 deletions read-file/read_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""Read a local file or remote URL."""

from __future__ import annotations

import json
import os
from argparse import ArgumentParser
from pathlib import Path
from typing import TYPE_CHECKING

import requests
import yaml
from requests.exceptions import HTTPError, MissingSchema

if TYPE_CHECKING:
from argparse import Namespace
from collections.abc import Sequence
from typing import Literal


def parse_args(argv: Sequence[str] | None = None) -> Namespace:
# parse CLI for inputs
parser = ArgumentParser()
parser.add_argument(
"file",
type=str,
help="Local path or remote URL to the file to read.",
)
parser.add_argument(
"--parser",
choices=["json", "yaml"],
help=(
"Parser to use for the file. "
"If not specified, the file content is returned as is."
),
)
parser.add_argument(
"--default",
type=str,
help=(
"Default value to use if the file is not found. "
"If not specified, an error is raised."
),
)
return parser.parse_args(argv)


def read_file(file: str | os.PathLike[str] | Path, default: str | None) -> str:
try:
response = requests.get(file)
response.raise_for_status()
except (HTTPError, MissingSchema):
# HTTPError: if the response status code is not ok
# MissingSchema: if the URL is not valid
try:
return Path(file).read_text()
except FileNotFoundError:
if default is None:
raise
return default
else:
return response.text


def parse_content(content: str, parser: Literal["json", "yaml"]) -> str:
# if a parser is defined we parse the content and dump it as JSON
if parser == "json":
content = json.loads(content)
return json.dumps(content)
elif parser == "yaml":
content = yaml.safe_load(content)
return json.dumps(content)
else:
raise ValueError("Parser not supported.")


def get_output(content: str) -> str:
return f"content<<GITHUB_OUTPUT_content\n{content}\nGITHUB_OUTPUT_content\n"


def dump_output(content: str) -> None:
if output := os.getenv("GITHUB_OUTPUT"):
with Path(output).open("a") as fh:
fh.write(get_output(content))


def main() -> None:
args = parse_args()

content = read_file(args.file, args.default)
if args.parser:
content = parse_content(content, args.parser)
dump_output(content)


if __name__ == "__main__":
main()
2 changes: 2 additions & 0 deletions read-file/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pyyaml==6.0.2
requests==2.32.3
Loading
Loading