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

Feature: Migrate docs #32

Merged
merged 109 commits into from
Jan 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
109 commits
Select commit Hold shift + click to select a range
fa927f6
migration logic
yanksyoon Dec 14, 2022
0c98f97
add metadata arg docstring
yanksyoon Dec 14, 2022
df98fb1
rename migration types
yanksyoon Dec 14, 2022
4ca1d35
document migration module
yanksyoon Dec 14, 2022
f2750ab
move retrieve topic to separate try except
yanksyoon Dec 14, 2022
1e17576
switch docstring attr order
yanksyoon Dec 14, 2022
e1dc927
separate migrate/reconcile flow
yanksyoon Dec 14, 2022
174c474
change run tests to run_reconcile tests
yanksyoon Dec 15, 2022
e0acbce
add migration control flow
yanksyoon Dec 15, 2022
243e32c
fix spelling and lint feedback
yanksyoon Dec 15, 2022
ad545c8
composite actions
yanksyoon Dec 15, 2022
8903445
github action path
yanksyoon Dec 15, 2022
d845d32
pip install requirements
yanksyoon Dec 15, 2022
e18af3d
fix typo
yanksyoon Dec 15, 2022
456bf57
disable PR and enable tmate for demo
yanksyoon Dec 15, 2022
61a08f4
remove tmate
yanksyoon Dec 15, 2022
3d4c249
env python
yanksyoon Dec 15, 2022
4bd5d94
map inputs to env vars
yanksyoon Dec 15, 2022
d4136bc
multiline run
yanksyoon Dec 15, 2022
921bd10
enable PR demo
yanksyoon Dec 15, 2022
64fa218
test for invalid parent directory
yanksyoon Dec 16, 2022
f8663d0
add git module
yanksyoon Dec 19, 2022
6f216a7
pull request module
yanksyoon Dec 20, 2022
01d73a7
add pull request to main control flow
yanksyoon Dec 20, 2022
9004668
add configure user test
yanksyoon Dec 21, 2022
4f31f47
fix integration test
yanksyoon Dec 21, 2022
e5cc3b2
add logs
yanksyoon Dec 23, 2022
a4e9772
change regex to string processing
yanksyoon Dec 23, 2022
f329061
refactor tests to use git fixtures
yanksyoon Dec 23, 2022
22cd4c2
makke github access token optional for migration flow
yanksyoon Dec 28, 2022
4e9d3db
add tests for dry run mode
yanksyoon Dec 28, 2022
828c72e
add tests for dry run mode in pull request module
yanksyoon Dec 28, 2022
646d9d9
revert to using docker action
yanksyoon Dec 28, 2022
15c2c1e
remove interpolation in description
yanksyoon Dec 29, 2022
81adaa2
add git to docker image
yanksyoon Dec 29, 2022
01a4a80
add git to docker image
yanksyoon Dec 29, 2022
ee046e8
fix spelling
yanksyoon Dec 29, 2022
a7858ee
add missing docstring
yanksyoon Dec 29, 2022
96e2d01
fix integratino test w/ dry run mode
yanksyoon Dec 29, 2022
0010b22
fix github repo get pulls base
yanksyoon Dec 29, 2022
e240068
add git before to self test
yanksyoon Dec 29, 2022
e284fec
fix pull requests fetching
yanksyoon Dec 30, 2022
cea5267
fix tests to match PR html url
yanksyoon Dec 30, 2022
5182b94
improve comments per pyupgrade
yanksyoon Dec 30, 2022
9ea1daf
remove unused import
yanksyoon Dec 30, 2022
2cd56e4
Merge remote-tracking branch 'origin/main' into feature/migration
yanksyoon Dec 30, 2022
a5e941b
merge changes in main
yanksyoon Dec 30, 2022
79b6fb6
refurb code feedback
yanksyoon Dec 30, 2022
e225279
reformat doc
yanksyoon Jan 2, 2023
434af4f
change docs italics
yanksyoon Jan 2, 2023
79c7842
add fail on migration fail behavior
yanksyoon Jan 2, 2023
548b17d
fix typo asssert
yanksyoon Jan 2, 2023
8fd598d
remove dry run mode in migration mode
yanksyoon Jan 3, 2023
b179f64
separate out migration tests
yanksyoon Jan 3, 2023
3893fb4
remove default value
yanksyoon Jan 4, 2023
5cdfc89
add mypy type checks
yanksyoon Jan 4, 2023
6e1e599
refactor pull request module as RepositoryClient
yanksyoon Jan 4, 2023
57ca99b
refactor pull_request module to repository client
yanksyoon Jan 5, 2023
24d5540
unpin patch versions
yanksyoon Jan 5, 2023
eb2d41b
remove unused dependencies in fmt run
yanksyoon Jan 5, 2023
9a6e4d5
add missing types
yanksyoon Jan 5, 2023
da950fa
fix wrong GitPython pin version
yanksyoon Jan 6, 2023
95996b9
merge MirationReport and ActionReport
yanksyoon Jan 6, 2023
1738d1d
separate out migration integration test
yanksyoon Jan 6, 2023
8fec15c
refactor migration module
yanksyoon Jan 6, 2023
d8b4f19
fix iterable string
yanksyoon Jan 6, 2023
42c33e6
pin git version
yanksyoon Jan 6, 2023
2ce7375
remove newline
yanksyoon Jan 6, 2023
60fe7d6
remove support for custom branch names
yanksyoon Jan 6, 2023
b8f9557
add dry_run mode in migration description
yanksyoon Jan 6, 2023
3aeba0c
merge InvalidTableRowError with InputError
yanksyoon Jan 6, 2023
bfaa321
update docstrings related to metadata
yanksyoon Jan 6, 2023
2fc549f
rename term group depth to group level
yanksyoon Jan 6, 2023
bc3174d
fix typo
yanksyoon Jan 6, 2023
2d3619a
rename depth to level
yanksyoon Jan 6, 2023
f71153f
update to specific messages
yanksyoon Jan 6, 2023
8097c5b
add create gitkeep file
yanksyoon Jan 6, 2023
21b1655
Merge branch 'main' into feature/migration
yanksyoon Jan 6, 2023
8ce5393
add missing docstrings
yanksyoon Jan 6, 2023
c26da1f
pin git version
yanksyoon Jan 7, 2023
8cd0e56
pyupgrade & refurb feedback
yanksyoon Jan 8, 2023
a7b94f0
add migrate mark
yanksyoon Jan 8, 2023
540e948
unpin git patch versions
yanksyoon Jan 8, 2023
b3e0ad0
convert row to markdown for error message
yanksyoon Jan 9, 2023
c3ffa4d
add explanation for skipped docstring check
yanksyoon Jan 9, 2023
e70762f
change permissions to be executable
yanksyoon Jan 9, 2023
285e22d
rename testing init module
yanksyoon Jan 9, 2023
22cf669
move logging to control flow
yanksyoon Jan 9, 2023
9d5647d
refactor validation out of main loop
yanksyoon Jan 9, 2023
9a2c347
add additional comment on algorithm
yanksyoon Jan 9, 2023
cbb721c
improve docs readability
yanksyoon Jan 10, 2023
54c5964
update docstrings to make terms clearer
yanksyoon Jan 10, 2023
9201659
remove redundant test for missing docs
yanksyoon Jan 10, 2023
c75ec14
remove redundant marker
yanksyoon Jan 10, 2023
48c087f
group user inputs
yanksyoon Jan 10, 2023
a3c17f0
minor wording changes
yanksyoon Jan 10, 2023
ae3f414
remove redundant test case
yanksyoon Jan 10, 2023
b430b5c
add test for content after table
yanksyoon Jan 10, 2023
2fa5de7
minor feedback adjustments
yanksyoon Jan 10, 2023
3ed634d
separate repo and repo path
yanksyoon Jan 10, 2023
ca909a3
add github output path
yanksyoon Jan 10, 2023
c0d900e
add specific type hints
yanksyoon Jan 10, 2023
9bb9c15
apply table row factory
yanksyoon Jan 10, 2023
9b75028
group tests together
yanksyoon Jan 10, 2023
6d30fcd
minor docstring changes
yanksyoon Jan 10, 2023
7dbc67d
explicitly describe discourse_hostname
yanksyoon Jan 11, 2023
1227faa
separate out generator steps
yanksyoon Jan 11, 2023
8d4896e
rename tests for repository client
yanksyoon Jan 11, 2023
9547a6c
sort requirements alphabetically
yanksyoon Jan 12, 2023
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
1 change: 1 addition & 0 deletions .codespellignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pullrequest
jdkandersson marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 2 additions & 2 deletions .github/workflows/integration_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
secrets: inherit
with:
pre-run-script: tests/integration/pre_run.sh
modules: '["discourse", "init"]'
modules: '["discourse", "reconcile", "migrate"]'
self-tests:
runs-on: ubuntu-22.04
steps:
Expand Down Expand Up @@ -55,7 +55,7 @@ jobs:
run: echo '${{ steps.selfTestDraft.outputs.urls_with_actions }}'
- name: Check draft
run: |
sudo apt update && sudo apt install python3-pip
sudo apt update && sudo apt install python3-pip git
pip3 install -r requirements.txt
./discourse_check_cleanup.py --action check-draft --action-kwargs '{"expected_url_results": []}' '${{ steps.selfTestDraft.outputs.urls_with_actions }}' '${{ steps.selfTestDraft.outputs.discourse_config }}'
- name: Create self test
Expand Down
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
FROM python:3.10-slim

RUN apt-get update && apt-get install -y --no-install-recommends git=1:2.30.*

RUN mkdir /usr/src/app
WORKDIR /usr/src/app
COPY requirements.txt /usr/src/app
Expand Down
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ charmhub.

## Getting Started

### Sync docs

1. Create the `docs` folder in the repository.
2. Optionally, create a file `docs/index.md` for any content you would like to
display above the navigation table on discourse. This content does not get
Expand Down Expand Up @@ -75,6 +77,37 @@ charmhub.
is also available under the `index_url` output of the action. This needs to
be added to the `metadata.yaml` under the `docs` key.

### Migrate docs

1. Create a `docs` key in `metadata.yaml` with the link to the documentation on
charmhub.
2. Add the action to your desired workflow as mentioned in step 5 of
jdkandersson marked this conversation as resolved.
Show resolved Hide resolved
[Sync docs section](#sync-docs) with github_token. For example:

```yaml
jobs:
publish-docs:
name: Publish docs
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- name: Publish documentation
uses: canonical/upload-charm-docs@main
id: publishDocumentation
with:
discourse_host: discourse.charmhub.io
discourse_api_username: ${{ secrets.DISCOURSE_API_USERNAME }}
discourse_api_key: ${{ secrets.DISCOURSE_API_KEY }}
github_token: ${{ secrets.GITHUB_TOKEN }}
```

a branch name with `upload-charm-docs/migrate` will be created and a pull
request named `[upload-charm-docs] Migrate charm docs` will be created
towards the working branch the workflow was triggered with.
In order to ensure that the branches can be created successfully, please
make sure that there are no existing branches clashing with the name above.
Please note that `dry_run` parameter has no effect on migrate mode.

The action will now compare the discourse topics with the files and directories
under the `docs` directory and make any changes based on differences.
Additional recommended steps:
Expand Down
8 changes: 7 additions & 1 deletion action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ inputs:
required: false
type: boolean
discourse_host:
description: The discourse host name.
description: The base path(hostname) to the discourse server.
required: true
type: string
discourse_api_username:
Expand All @@ -34,6 +34,12 @@ inputs:
default: 41
required: false
type: integer
github_token:
description: |
The github access token (secrets.GITHUB_TOKEN) to create pull request on Github.
Required if running in migration mode.
required: false
type: string
outputs:
urls_with_actions:
description: |
Expand Down
2 changes: 1 addition & 1 deletion discourse_check_cleanup.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class Action(str, Enum):
CLEANUP = "cleanup"


def main():
def main() -> None:
"""Clean up created Discourse pages.

Raises:
Expand Down
97 changes: 72 additions & 25 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,53 +11,100 @@
import pathlib
from functools import partial

from src import run
from src import GETTING_STARTED, exceptions, run, types_
from src.discourse import create_discourse


def main():
"""Execute the action."""
logging.basicConfig(level=logging.INFO)
def _parse_env_vars() -> types_.UserInputs:
"""Instantiate user inputs from environment variables.

# Read input
Returns:
Wrapped user input variables.
"""
discourse_host = os.getenv("INPUT_DISCOURSE_HOST", "")
discourse_category_id = os.getenv("INPUT_DISCOURSE_CATEGORY_ID", "")
discourse_api_username = os.getenv("INPUT_DISCOURSE_API_USERNAME", "")
discourse_api_key = os.getenv("INPUT_DISCOURSE_API_KEY", "")
delete_topics = os.getenv("INPUT_DELETE_TOPICS") == "true"
dry_run = os.getenv("INPUT_DRY_RUN") == "true"
discourse_host = os.getenv("INPUT_DISCOURSE_HOST")
discourse_category_id = os.getenv("INPUT_DISCOURSE_CATEGORY_ID")
discourse_api_username = os.getenv("INPUT_DISCOURSE_API_USERNAME")
discourse_api_key = os.getenv("INPUT_DISCOURSE_API_KEY")
github_access_token = os.getenv("INPUT_GITHUB_TOKEN")

# Execute action
create_discourse_kwargs = {
"hostname": discourse_host,
"category_id": discourse_category_id,
"api_username": discourse_api_username,
"api_key": discourse_api_key,
}
discourse = create_discourse(**create_discourse_kwargs)
urls_with_actions_dict = run(
base_path=pathlib.Path(),
discourse=discourse,
dry_run=dry_run,
return types_.UserInputs(
discourse_hostname=discourse_host,
discourse_category_id=discourse_category_id,
discourse_api_username=discourse_api_username,
discourse_api_key=discourse_api_key,
delete_pages=delete_topics,
dry_run=dry_run,
github_access_token=github_access_token,
jdkandersson marked this conversation as resolved.
Show resolved Hide resolved
)

# Write output
github_output = pathlib.Path(os.getenv("GITHUB_OUTPUT"))

def _write_github_output(
urls_with_actions_dict: dict[str, str], user_inputs: types_.UserInputs
) -> None:
"""Writes results produced by the action to github_output.

Args:
urls_with_actions_dict: key value pairs of link to result of action.
user_inputs: parsed input variables used to run the action.

Raises:
InputError: if not running inside a github actions environment.
"""
github_output = os.getenv("GITHUB_OUTPUT")
if not github_output:
raise exceptions.InputError(
f"Invalid 'GITHUB_OUTPUT' input, it must be non-empty, got {github_output=!r}"
f"This action is intended to run inside github-actions. {GETTING_STARTED}"
)

github_output_path = pathlib.Path(github_output)
compact_json = partial(json.dumps, separators=(",", ":"))
urls_with_actions = compact_json(urls_with_actions_dict)
if urls_with_actions_dict:
*_, index_url = urls_with_actions_dict.keys()
else:
index_url = ""
discourse_config = compact_json(create_discourse_kwargs)
github_output.write_text(
discourse_config = compact_json(
{
"hostname": user_inputs.discourse_hostname,
"category_id": user_inputs.discourse_category_id,
"api_username": user_inputs.discourse_api_username,
"api_key": user_inputs.discourse_api_key,
}
)
github_output_path.write_text(
f"urls_with_actions={urls_with_actions}\n"
f"index_url={index_url}\n"
f"discourse_config={discourse_config}\n",
encoding="utf-8",
)


def main() -> None:
"""Execute the action."""
logging.basicConfig(level=logging.INFO)

# Read input
user_inputs = _parse_env_vars()

# Execute action
discourse = create_discourse(
hostname=user_inputs.discourse_hostname,
category_id=user_inputs.discourse_category_id,
api_username=user_inputs.discourse_api_username,
api_key=user_inputs.discourse_api_key,
)
urls_with_actions_dict = run(
base_path=pathlib.Path(),
discourse=discourse,
user_inputs=user_inputs,
)

# Write output
_write_github_output(urls_with_actions_dict=urls_with_actions_dict, user_inputs=user_inputs)


if __name__ == "__main__":
main()
8 changes: 7 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ show_missing = true
[tool.pytest.ini_options]
minversion = "6.0"
log_cli_level = "INFO"
markers = ["init", "discourse"]
markers = ["reconcile", "migrate", "discourse"]

# Formatting tools configuration
[tool.black]
Expand Down Expand Up @@ -49,3 +49,9 @@ copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s"

[tool.mypy]
ignore_missing_imports = true
check_untyped_defs = true
disallow_untyped_defs = true

[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
GitPython>=3.1,<3.2
pydiscourse>=1.3,<1.4
PyGithub>=1.57,<1.58
PyYAML>=6.0,<6.1
requests>=2.28,<2.29
97 changes: 89 additions & 8 deletions src/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,36 @@
from .action import DRY_RUN_NAVLINK_LINK, FAIL_NAVLINK_LINK
from .action import run_all as run_all_actions
from .discourse import Discourse
from .docs_directory import has_docs_directory
from .docs_directory import read as read_docs_directory
from .index import DOCUMENTATION_FOLDER_NAME
from .exceptions import InputError
from .index import DOCUMENTATION_FOLDER_NAME, contents_from_page
from .index import get as get_index
from .metadata import get as get_metadata
from .migration import run as migrate_contents
from .navigation_table import from_page as navigation_table_from_page
from .pull_request import RepositoryClient, create_pull_request, create_repository_client
from .reconcile import run as run_reconcile
from .types_ import ActionResult, Metadata, UserInputs

GETTING_STARTED = (
"To get started with upload-charm-docs, "
"please refer to https://github.com/canonical/upload-charm-docs#getting-started"
)

def run(

def _run_reconcile(
base_path: Path,
metadata: Metadata,
discourse: Discourse,
dry_run: bool,
delete_pages: bool,
) -> dict[str, str]:
"""Upload the documentation to charmhub.

Args:
base_path: The base path to look for the metadata file in.
base_path: The base path of the repository.
metadata: Information about the charm.
discourse: A client to the documentation server.
dry_run: If enabled, only log the action that would be taken.
delete_pages: Whether to delete pages that are no longer needed.
Expand All @@ -34,7 +46,6 @@ def run(
All the URLs that had an action with the result of that action.

"""
metadata = get_metadata(base_path)
index = get_index(metadata=metadata, base_path=base_path, server_client=discourse)
path_infos = read_docs_directory(docs_path=base_path / DOCUMENTATION_FOLDER_NAME)
server_content = (
Expand All @@ -50,9 +61,79 @@ def run(
delete_pages=delete_pages,
)
return {
report.url: report.result
str(report.location): report.result
for report in reports
if report.url is not None
and report.url != DRY_RUN_NAVLINK_LINK
and report.url != FAIL_NAVLINK_LINK
if report.location is not None
and report.location != DRY_RUN_NAVLINK_LINK
and report.location != FAIL_NAVLINK_LINK
}


def _run_migrate(
base_path: Path, metadata: Metadata, discourse: Discourse, repository: RepositoryClient
) -> dict[str, str]:
"""Migrate existing docs from charmhub to local repository.

Args:
base_path: The base path of the repository.
metadata: Information about the charm.
discourse: A client to the documentation server.
repository: Repository client for managing both local and remote git repositories.

Returns:
A single key-value pair dictionary containing a link to the Pull Request containing
migrated documentation as key and successful action result as value.
"""
index = get_index(metadata=metadata, base_path=base_path, server_client=discourse)
server_content = (
jdkandersson marked this conversation as resolved.
Show resolved Hide resolved
index.server.content if index.server is not None and index.server.content else ""
)
index_content = contents_from_page(server_content)
table_rows = navigation_table_from_page(page=server_content, discourse=discourse)
migrate_contents(
table_rows=table_rows,
index_content=index_content,
discourse=discourse,
docs_path=base_path / DOCUMENTATION_FOLDER_NAME,
)

pr_link = create_pull_request(repository=repository)
yanksyoon marked this conversation as resolved.
Show resolved Hide resolved

return {pr_link: ActionResult.SUCCESS}


def run(base_path: Path, discourse: Discourse, user_inputs: UserInputs) -> dict[str, str]:
"""Interact with charmhub to upload documentation or migrate to local repository.

Args:
base_path: The base path to look for the metadata file in.
discourse: A client to the documentation server.
user_inputs: Configurable inputs for running upload-charm-docs.

Raises:
InputError: if no valid running mode is matched.

Returns:
All the URLs that had an action with the result of that action.
"""
metadata = get_metadata(base_path)
has_docs_dir = has_docs_directory(base_path=base_path)
if metadata.docs and not has_docs_dir:
repository = create_repository_client(
access_token=user_inputs.github_access_token, base_path=base_path
)
return _run_migrate(
base_path=base_path,
metadata=metadata,
discourse=discourse,
repository=repository,
)
if has_docs_dir:
return _run_reconcile(
base_path=base_path,
metadata=metadata,
discourse=discourse,
dry_run=user_inputs.dry_run,
delete_pages=user_inputs.delete_pages,
)
raise InputError(GETTING_STARTED)
Loading