diff --git a/.github/actions/update-charm-pins/action.yaml b/.github/actions/update-charm-pins/action.yaml new file mode 100644 index 000000000..aa892aefc --- /dev/null +++ b/.github/actions/update-charm-pins/action.yaml @@ -0,0 +1,29 @@ +--- +name: Update Charm Pins +description: Updates pinned versions of external charms we use to test our changes against to prevent regressions +author: Dima Tisnek +branding: + icon: activity + color: orange + +inputs: + workflows: + description: Whitespace-separated paths to the local workflow file, relative to repository root + required: true + gh-pat: + description: Personal access token to check out external repos from github + required: true + +runs: + using: composite + steps: + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - run: python -m pip install -r .github/actions/update-charm-pins/requirements.txt + shell: bash + - run: python .github/actions/update-charm-pins/main.py '${{ inputs.workflows }}' + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.gh-pat }} diff --git a/.github/actions/update-charm-pins/main.py b/.github/actions/update-charm-pins/main.py new file mode 100644 index 000000000..7cdf43d31 --- /dev/null +++ b/.github/actions/update-charm-pins/main.py @@ -0,0 +1,55 @@ +# Copyright 2024 Canonical Ltd. + +"""Updates pinned versions of charms in tests.""" + +import logging +import os +import sys + +from httpx import Client +from ruamel.yaml import YAML + +yaml = YAML(typ="rt") +yaml.indent(mapping=2, sequence=4, offset=2) + +github = Client( + base_url="https://api.github.com/repos", + headers={ + "Authorization": f'token {os.getenv("GITHUB_TOKEN")}', + "Accept": "application/vnd.github.v3+json", + }, +) + + +def update_charm_pins(workflow): + """Update pinned versions of charms in the given GitHub Actions workflow.""" + with open(workflow) as file: + doc = yaml.load(file) + + # Assume the workflow has a single job or the first job is parameterized with charm repos + job_name = next(iter(doc["jobs"])) + + for idx, item in enumerate(doc["jobs"][job_name]["strategy"]["matrix"]["include"]): + charm_repo = item["charm-repo"] + commit = github.get(f"{charm_repo}/commits").raise_for_status().json()[0] + data = github.get(f"{charm_repo}/tags").raise_for_status().json() + comment = " ".join( + [tag["name"] for tag in data if tag["commit"]["sha"] == commit["sha"]] + + [commit["commit"]["committer"]["date"]] + ) + + # A YAML node, as opposed to a plain value, can be updated in place to tweak comments + node = doc.mlget( + ["jobs", job_name, "strategy", "matrix", "include", idx], list_ok=True + ) + node["commit"] = commit["sha"] + node.yaml_add_eol_comment(comment, key="commit") + + with open(workflow, "w") as file: + yaml.dump(doc, file) + + +if __name__ == "__main__": + logging.basicConfig(level="INFO") + for workflow in " ".join(sys.argv[1:]).split(): + update_charm_pins(workflow) diff --git a/.github/actions/update-charm-pins/readme.md b/.github/actions/update-charm-pins/readme.md new file mode 100644 index 000000000..2b5e87126 --- /dev/null +++ b/.github/actions/update-charm-pins/readme.md @@ -0,0 +1,27 @@ +# Update Charm Pins + +## GitHub Actions Usage + +Inputs: + +- `workflows`: space or newline-separated list of workflow YAML files relative to this repository root +- `gh-pat`: personal access token to query external repositories hosted at GitHub + +This action will update the `workflows` in the current checkout. It is the responsibility of the caller +to do something with these changes. + +## Local Usage + +```command +# set up a venv and install the deps +pip install -r requirements.txt + +# set the GITHUB_TOKEN env var with a personal access token +export GITHUB_TOKEN=ghp_0123456789 + +# run the script +python main.py path-to/.github/workflows/one.yaml path-to/.github/workflows/another.yaml + +# check the modifications in the current branch +git diff +``` diff --git a/.github/actions/update-charm-pins/requirements.txt b/.github/actions/update-charm-pins/requirements.txt new file mode 100644 index 000000000..424fcb0d4 --- /dev/null +++ b/.github/actions/update-charm-pins/requirements.txt @@ -0,0 +1,2 @@ +ruamel.yaml==0.18.6 +httpx==0.27.0 diff --git a/.github/workflows/charmcraft-pack.yaml b/.github/workflows/charmcraft-pack.yaml index 563c291a4..bd910f4d8 100644 --- a/.github/workflows/charmcraft-pack.yaml +++ b/.github/workflows/charmcraft-pack.yaml @@ -6,33 +6,39 @@ jobs: charmcraft-pack: runs-on: ubuntu-22.04 + strategy: + matrix: + include: + - charm-repo: jnsgruk/hello-kubecon + commit: dbd133466dde59ee64f20a732a8f3d2e560ec3b8 # 2023-07-03T14:09:38Z steps: - - name: Checkout test charm repository - uses: actions/checkout@v4 - with: - repository: jnsgruk/hello-kubecon + - name: Checkout test charm repository + uses: actions/checkout@v4 + with: + repository: ${{ matrix.charm-repo }} + ref: ${{ matrix.commit }} - - name: Update 'ops' dependency in test charm to latest - run: | - sed -i -e "/^ops[ ><=]/d" -e "/canonical\/operator/d" -e "/#egg=ops/d" requirements.txt - if [ -z "${{ github.event.pull_request.head.sha }}" ] - then - echo -e "\ngit+$GITHUB_SERVER_URL/$GITHUB_REPOSITORY@$GITHUB_SHA#egg=ops" >> requirements.txt - else - # If on a PR, we need to reference the PR branch's repo and commit (not the GITHUB_SHA - # temporary merge commit), because charmcraft pack does a git checkout which - # can't see the temporary merge commit. - echo -e "\ngit+${{ github.event.pull_request.head.repo.clone_url }}@${{ github.event.pull_request.head.sha }}#egg=ops" >> requirements.txt - fi - cat requirements.txt + - name: Update 'ops' dependency in test charm to latest + run: | + sed -i -e "/^ops[ ><=]/d" -e "/canonical\/operator/d" -e "/#egg=ops/d" requirements.txt + if [ -z "${{ github.event.pull_request.head.sha }}" ] + then + echo -e "\ngit+$GITHUB_SERVER_URL/$GITHUB_REPOSITORY@$GITHUB_SHA#egg=ops" >> requirements.txt + else + # If on a PR, we need to reference the PR branch's repo and commit (not the GITHUB_SHA + # temporary merge commit), because charmcraft pack does a git checkout which + # can't see the temporary merge commit. + echo -e "\ngit+${{ github.event.pull_request.head.repo.clone_url }}@${{ github.event.pull_request.head.sha }}#egg=ops" >> requirements.txt + fi + cat requirements.txt - - name: Set up LXD - uses: canonical/setup-lxd@7be523c4c2724a31218a627809044c6a2f0870ad - with: - channel: 5.0/stable + - name: Set up LXD + uses: canonical/setup-lxd@7be523c4c2724a31218a627809044c6a2f0870ad + with: + channel: 5.0/stable - - name: Install charmcraft - run: sudo snap install charmcraft --classic + - name: Install charmcraft + run: sudo snap install charmcraft --classic - - name: Pack the charm - run: sudo charmcraft pack --verbose + - name: Pack the charm + run: sudo charmcraft pack --verbose diff --git a/.github/workflows/db-charm-tests.yaml b/.github/workflows/db-charm-tests.yaml index a8cdbafce..f4c72fd0b 100644 --- a/.github/workflows/db-charm-tests.yaml +++ b/.github/workflows/db-charm-tests.yaml @@ -8,19 +8,21 @@ jobs: strategy: fail-fast: false matrix: - charm-repo: - - "canonical/postgresql-operator" - - "canonical/postgresql-k8s-operator" - - "canonical/mysql-operator" -# TODO: uncomment once secrets issues are fixed in this charm: -# https://github.com/canonical/mysql-k8s-operator/pull/371 -# - "canonical/mysql-k8s-operator" - + include: + - charm-repo: canonical/postgresql-operator + commit: 4feeaeee102cbf5e3dada3c05d44e0495ca68f9a # rev409 2024-05-21T12:52:24Z + - charm-repo: canonical/postgresql-k8s-operator + commit: 1a25c3929747beea7e78467b169b2b345b29d470 # 2024-05-21T12:40:19Z + - charm-repo: canonical/mysql-operator + commit: 19633f3e904d1c3296477b3df191d1ca265fc0d5 # rev234 2024-05-06T12:13:54Z + - charm-repo: canonical/mysql-k8s-operator + commit: 6c09910bc3bd88eb632793d08fa17340c6903cb2 # rev138 2024-05-01T18:08:13Z steps: - name: Checkout the ${{ matrix.charm-repo }} repository uses: actions/checkout@v4 with: repository: ${{ matrix.charm-repo }} + ref: ${{ matrix.commit }} - name: Checkout the operator repository uses: actions/checkout@v4 diff --git a/.github/workflows/hello-charm-tests.yaml b/.github/workflows/hello-charm-tests.yaml index b4970af03..3a756af3c 100644 --- a/.github/workflows/hello-charm-tests.yaml +++ b/.github/workflows/hello-charm-tests.yaml @@ -8,20 +8,22 @@ jobs: strategy: matrix: - charm-repo: - - "jnsgruk/hello-kubecon" - - "juju/hello-juju-charm" - + include: + - charm-repo: jnsgruk/hello-kubecon + commit: dbd133466dde59ee64f20a732a8f3d2e560ec3b8 # 2023-07-03T14:09:38Z + - charm-repo: juju/hello-juju-charm + commit: 046b8ce758660d5aa9cf05207e2370fcbab688d0 # 2021-12-16T10:10:24Z steps: - name: Set up Python 3.8 uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: '3.8' - name: Checkout the ${{ matrix.charm-repo }} repository uses: actions/checkout@v4 with: repository: ${{ matrix.charm-repo }} + ref: ${{ matrix.commit }} - name: Remove 'ops' from charm requirements.txt run: | diff --git a/.github/workflows/observability-charm-tests.yaml b/.github/workflows/observability-charm-tests.yaml index 709386a2d..33bb64f16 100644 --- a/.github/workflows/observability-charm-tests.yaml +++ b/.github/workflows/observability-charm-tests.yaml @@ -9,16 +9,19 @@ jobs: strategy: fail-fast: false matrix: - charm-repo: - - "canonical/alertmanager-k8s-operator" - - "canonical/prometheus-k8s-operator" - - "canonical/grafana-k8s-operator" - + include: + - charm-repo: canonical/alertmanager-k8s-operator + commit: 90b85c79dfdeeeedcc538c92dcbc26fb2b931088 # rev114 2024-05-23T12:09:22Z + - charm-repo: canonical/prometheus-k8s-operator + commit: 41b10003b2e7aba34e26fa387af4297d97ecb535 # rev189 2024-05-21T21:25:20Z + - charm-repo: canonical/grafana-k8s-operator + commit: ec74910fc60848594ce44da3b549ad18aa6528aa # rev113 2024-05-21T14:31:49Z steps: - name: Checkout the ${{ matrix.charm-repo }} repository uses: actions/checkout@v4 with: repository: ${{ matrix.charm-repo }} + ref: ${{ matrix.commit }} - name: Update 'ops' dependency in test charm to latest run: | diff --git a/.github/workflows/update-charm-tests.yaml b/.github/workflows/update-charm-tests.yaml new file mode 100644 index 000000000..15bc0ffe7 --- /dev/null +++ b/.github/workflows/update-charm-tests.yaml @@ -0,0 +1,53 @@ +--- +name: Update Charm Pins + +on: + # NOTE: to avoid infinite loop, exclude the branch created by this workflow if triggering on push or pull_request + workflow_dispatch: + schedule: + - cron: '0 18 * * SUN' # Sunday 6pm UTC, before international day starts + +jobs: + update-pins: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.UPDATE_CHARM_PINS_ACCESS_TOKEN }} + + - uses: ./.github/actions/update-charm-pins + with: + # Whitespace (null) separated string, as workflow inputs are always plain values + workflows: |- + .github/workflows/db-charm-tests.yaml + .github/workflows/hello-charm-tests.yaml + .github/workflows/charmcraft-pack.yaml + .github/workflows/observability-charm-tests.yaml + gh-pat: ${{ secrets.UPDATE_CHARM_PINS_ACCESS_TOKEN }} + + - run: | + # Force-push pin changes to the branch + echo "New changes in charm pins" + git --no-pager diff + git config --global user.name "github-actions" + git config --global user.email "github-actions@github.com" + git switch -C auto-update-external-charm-pins + git commit --allow-empty -am "chore: update charm pins" + echo "Total changes in charm pins" + git --no-pager diff main HEAD + git push -f --set-upstream origin auto-update-external-charm-pins + - run: | + # Ensure a PR if there are changes, no PR otherwise + PR=$(gh pr list --state open --head auto-update-external-charm-pins --json number -q '.[0].number') + CHANGES=$(git --no-pager diff --stat main HEAD) + echo "Existing PR? $PR" + echo "Changes? $CHANGES" + if [[ -n "$PR" && -z "$CHANGES" ]]; then + echo "Closing #$PR as stale" + gh pr close -c stale "$PR"; + elif [[ -z "$PR" && -n "$CHANGES" ]]; then + echo "Opening new PR" + gh pr create --base main --head auto-update-external-charm-pins --title "chore: update charm pins" --body "This is an automated PR to update pins of the external repositories that the operator framework is tested against"; + fi + env: + GITHUB_TOKEN: ${{ secrets.UPDATE_CHARM_PINS_ACCESS_TOKEN }} diff --git a/.gitignore b/.gitignore index 350428eb3..47edccb41 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,12 @@ venv .vscode .coverage /.tox +.*.swp + +# Tokens and settings for `act` to run GHA locally +.env +.envrc +.secrets # Build artifacts /dist diff --git a/HACKING.md b/HACKING.md index f8a008d35..b169960c8 100644 --- a/HACKING.md +++ b/HACKING.md @@ -155,6 +155,11 @@ your charm to a controller using that version of Juju. For example, with microk8 3. Run `GOBIN=/path/to/your/juju/_build/linux_amd64/bin:$GOBIN /path/to/your/juju bootstrap` 4. Add a model and deploy your charm as normal +### Regression testing against existing charms + +We rely on automation to [update charm pins](.github/actions/update-charm-pins/) of +a bunch of charms that use the operator framework. The script can be run locally too. + # Documentation In general, new functionality