diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..b68a296 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,41 @@ +name: Bug report +description: File a bug report. +title: "[BUG]: " +labels: ["bug"] + +body: + - type: markdown + attributes: + value: | + Bug report: + - type: textarea + id: description + attributes: + label: Provide a brief description of the bug. + - type: textarea + id: expected + attributes: + label: Describe what you expected to happen and what actually happened. + - type: textarea + id: reproduction + attributes: + label: Outline the steps that lead to the bug's occurrence. Be specific and provide a clear sequence of actions. + - type: input + id: version + attributes: + label: bec_widgets version + description: which version of BEC widgets was running? + - type: input + id: bec-version + attributes: + label: bec core version + description: which version of BEC core was running? + - type: textarea + id: extra + attributes: + label: Any extra info / data? e.g. log output... + - type: input + id: issues + attributes: + label: Related issues + description: please tag any related issues \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/documentation_update.md b/.github/ISSUE_TEMPLATE/documentation_update.md new file mode 100644 index 0000000..cb03f6b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation_update.md @@ -0,0 +1,37 @@ +--- +name: Documentation update request +about: Suggest an update to the docs +title: '[DOCS]: ' +type: documentation +label: documentation +assignees: '' + +--- + +## Documentation Section + +[Specify the section or page of the documentation that needs updating] + +## Current Information + +[Provide the current information in the documentation that needs to be updated] + +## Proposed Update + +[Describe the proposed update or correction. Be specific about the changes that need to be made] + +## Reason for Update + +[Explain the reason for the documentation update. Include any recent changes, new features, or corrections that necessitate the update] + +## Additional Context + +[Include any additional context or information that can help the documentation team understand the update better] + +## Attachments + +[Attach any files, screenshots, or references that can assist in making the documentation update] + +## Priority + +[Assign a priority level to the documentation update based on its urgency. Use a scale such as Low, Medium, High] diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..0433812 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,49 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '[FEAT]: ' +type: feature +label: feature +assignees: '' + +--- + +## Feature Summary + +[Provide a brief and clear summary of the new feature you are requesting] + +## Problem Description + +[Explain the problem or need that this feature aims to address. Be specific about the issues or gaps in the current functionality] + +## Use Case + +[Describe a real-world scenario or use case where this feature would be beneficial. Explain how it would improve the user experience or workflow] + +## Proposed Solution + +[If you have a specific solution in mind, describe it here. Explain how it would work and how it would address the problem described above] + +## Benefits + +[Explain the benefits and advantages of implementing this feature. Highlight how it adds value to the product or improves user satisfaction] + +## Alternatives Considered + +[If you've considered alternative solutions or workarounds, mention them here. Explain why the proposed feature is the preferred option] + +## Impact on Existing Functionality + +[Discuss how the new feature might impact or interact with existing features. Address any potential conflicts or dependencies] + +## Priority + +[Assign a priority level to the feature request based on its importance. Use a scale such as Low, Medium, High] + +## Attachments + +[Include any relevant attachments, such as sketches, diagrams, or references that can help the development team understand your feature request better] + +## Additional Information + +[Provide any additional information that might be relevant to the feature request, such as user feedback, market trends, or similar features in other products] diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 0000000..0e56de5 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1,33 @@ +## Description + +[Provide a brief description of the changes introduced by this pull request.] + +## Related Issues + +[Cite any related issues or feature requests that are addressed or resolved by this pull request. Link the associated issue, for example, with `fixes #123` or `closes #123`.] + +## Type of Change + +- Change 1 +- Change 2 + +## How to test + +- Run unit tests +- Open [widget] in designer and play around with the properties + +## Potential side effects + +[Describe any potential side effects or risks of merging this PR.] + +## Screenshots / GIFs (if applicable) + +[Include any relevant screenshots or GIFs to showcase the changes made.] + +## Additional Comments + +[Add any additional comments or information that may be helpful for reviewers.] + +## Definition of Done +- [ ] Documentation is up-to-date. + diff --git a/.github/actions/ophyd_devices_install/action.yml b/.github/actions/ophyd_devices_install/action.yml new file mode 100644 index 0000000..2dbb73e --- /dev/null +++ b/.github/actions/ophyd_devices_install/action.yml @@ -0,0 +1,45 @@ +name: "Ophyd Devices Install" +description: "Install BEC Widgets and related os dependencies" +inputs: + BEC_CORE_BRANCH: # id of input + required: false + default: "main" + description: "Branch of BEC Core to install" + OPHYD_DEVICES_BRANCH: # id of input + required: false + default: "main" + description: "Branch of Ophyd Devices to install" + PYTHON_VERSION: # id of input + required: false + default: "3.11" + description: "Python version to use" + +runs: + using: "composite" + steps: + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.PYTHON_VERSION }} + + - name: Checkout BEC Core + uses: actions/checkout@v4 + with: + repository: bec-project/bec + ref: ${{ inputs.BEC_CORE_BRANCH }} + path: ./bec + + - name: Checkout Ophyd Devices + uses: actions/checkout@v4 + with: + repository: bec-project/ophyd_devices + ref: ${{ inputs.OPHYD_DEVICES_BRANCH }} + path: ./ophyd_devices + + - name: Install Python dependencies + shell: bash + run: | + pip install uv + uv pip install --system -e ./ophyd_devices[dev] + uv pip install --system -e ./bec/bec_lib[dev] + uv pip install --system -e ./bec/bec_server[dev] diff --git a/.github/scripts/pr_issue_sync/pr_issue_sync.py b/.github/scripts/pr_issue_sync/pr_issue_sync.py new file mode 100644 index 0000000..3b90b26 --- /dev/null +++ b/.github/scripts/pr_issue_sync/pr_issue_sync.py @@ -0,0 +1,342 @@ +import functools +import os +from typing import Literal + +import requests +from github import Github +from pydantic import BaseModel + + +class GHConfig(BaseModel): + token: str + organization: str + repository: str + project_number: int + graphql_url: str + rest_url: str + headers: dict + + +class ProjectItemHandler: + """ + A class to handle GitHub project items. + """ + + def __init__(self, gh_config: GHConfig): + self.gh_config = gh_config + self.gh = Github(gh_config.token) + self.repo = self.gh.get_repo(f"{gh_config.organization}/{gh_config.repository}") + self.project_node_id = self.get_project_node_id() + + def set_issue_status( + self, + status: Literal[ + "Selected for Development", + "Weekly Backlog", + "In Development", + "Ready For Review", + "On Hold", + "Done", + ], + issue_number: int | None = None, + issue_node_id: str | None = None, + ): + """ + Set the status field of a GitHub issue in the project. + + Args: + status (str): The status to set. Must be one of the predefined statuses. + issue_number (int, optional): The issue number. If not provided, issue_node_id must be provided. + issue_node_id (str, optional): The issue node ID. If not provided, issue_number must be provided. + """ + if not issue_number and not issue_node_id: + raise ValueError("Either issue_number or issue_node_id must be provided.") + if issue_number and issue_node_id: + raise ValueError("Only one of issue_number or issue_node_id must be provided.") + if issue_number is not None: + issue = self.repo.get_issue(issue_number) + issue_id = self.get_issue_info(issue.node_id)[0]["id"] + else: + issue_id = issue_node_id + field_id, option_id = self.get_status_field_id(field_name=status) + self.set_field_option(issue_id, field_id, option_id) + + def run_graphql(self, query: str, variables: dict) -> dict: + """ + Execute a GraphQL query against the GitHub API. + + Args: + query (str): The GraphQL query to execute. + variables (dict): The variables to pass to the query. + + Returns: + dict: The response from the GitHub API. + """ + response = requests.post( + self.gh_config.graphql_url, + json={"query": query, "variables": variables}, + headers=self.gh_config.headers, + timeout=10, + ) + if response.status_code != 200: + raise Exception( + f"Query failed with status code {response.status_code}: {response.text}" + ) + return response.json() + + def get_project_node_id(self): + """ + Retrieve the project node ID from the GitHub API. + """ + query = """ + query($owner: String!, $number: Int!) { + organization(login: $owner) { + projectV2(number: $number) { + id + } + } + } + """ + variables = {"owner": self.gh_config.organization, "number": self.gh_config.project_number} + resp = self.run_graphql(query, variables) + return resp["data"]["organization"]["projectV2"]["id"] + + def get_issue_info(self, issue_node_id: str): + """ + Get the project-related information for a given issue node ID. + + Args: + issue_node_id (str): The node ID of the issue. Please note that this is not the issue number and typically starts with "I". + + Returns: + list[dict]: A list of project items associated with the issue. + """ + query = """ + query($issueId: ID!) { + node(id: $issueId) { + ... on Issue { + projectItems(first: 10) { + nodes { + project { + id + title + } + id + fieldValues(first: 20) { + nodes { + ... on ProjectV2ItemFieldSingleSelectValue { + name + field { + ... on ProjectV2SingleSelectField { + name + } + } + } + } + } + } + } + } + } + } + """ + variables = {"issueId": issue_node_id} + resp = self.run_graphql(query, variables) + return resp["data"]["node"]["projectItems"]["nodes"] + + def get_status_field_id( + self, + field_name: Literal[ + "Selected for Development", + "Weekly Backlog", + "In Development", + "Ready For Review", + "On Hold", + "Done", + ], + ) -> tuple[str, str]: + """ + Get the status field ID and option ID for the given field name in the project. + + Args: + field_name (str): The name of the field to retrieve. + Must be one of the predefined statuses. + + Returns: + tuple[str, str]: A tuple containing the field ID and option ID. + """ + field_id = None + option_id = None + project_fields = self.get_project_fields() + for field in project_fields: + if field["name"] != "Status": + continue + field_id = field["id"] + for option in field["options"]: + if option["name"] == field_name: + option_id = option["id"] + break + if not field_id or not option_id: + raise ValueError(f"Field '{field_name}' not found in project fields.") + + return field_id, option_id + + def set_field_option(self, item_id, field_id, option_id): + """ + Set the option of a project item for a single-select field. + + Args: + item_id (str): The ID of the project item to update. + field_id (str): The ID of the field to update. + option_id (str): The ID of the option to set. + """ + + mutation = """ + mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) { + updateProjectV2ItemFieldValue( + input: { + projectId: $projectId + itemId: $itemId + fieldId: $fieldId + value: { singleSelectOptionId: $optionId } + } + ) { + projectV2Item { + id + } + } + } + """ + variables = { + "projectId": self.project_node_id, + "itemId": item_id, + "fieldId": field_id, + "optionId": option_id, + } + return self.run_graphql(mutation, variables) + + @functools.lru_cache(maxsize=1) + def get_project_fields(self) -> list[dict]: + """ + Get the available fields in the project. + This method caches the result to avoid multiple API calls. + + Returns: + list[dict]: A list of fields in the project. + """ + + query = """ + query($projectId: ID!) { + node(id: $projectId) { + ... on ProjectV2 { + fields(first: 50) { + nodes { + ... on ProjectV2SingleSelectField { + id + name + options { + id + name + } + } + } + } + } + } + } + """ + variables = {"projectId": self.project_node_id} + resp = self.run_graphql(query, variables) + return list(filter(bool, resp["data"]["node"]["fields"]["nodes"])) + + def get_pull_request_linked_issues(self, pr_number: int) -> list[dict]: + """ + Get the linked issues of a pull request. + + Args: + pr_number (int): The pull request number. + + Returns: + list[dict]: A list of linked issues. + """ + query = """ + query($number: Int!, $owner: String!, $repo: String!) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $number) { + id + closingIssuesReferences(first: 50) { + edges { + node { + id + body + number + title + } + } + } + } + } + } + """ + variables = { + "number": pr_number, + "owner": self.gh_config.organization, + "repo": self.gh_config.repository, + } + resp = self.run_graphql(query, variables) + edges = resp["data"]["repository"]["pullRequest"]["closingIssuesReferences"]["edges"] + return [edge["node"] for edge in edges if edge.get("node")] + + +def main(): + # GitHub settings + token = os.getenv("TOKEN") + org = os.getenv("ORG") + repo = os.getenv("REPO") + project_number = os.getenv("PROJECT_NUMBER") + pr_number = os.getenv("PR_NUMBER") + + if not token: + raise ValueError("GitHub token is not set. Please set the TOKEN environment variable.") + if not org: + raise ValueError("GitHub organization is not set. Please set the ORG environment variable.") + if not repo: + raise ValueError("GitHub repository is not set. Please set the REPO environment variable.") + if not project_number: + raise ValueError( + "GitHub project number is not set. Please set the PROJECT_NUMBER environment variable." + ) + if not pr_number: + raise ValueError( + "Pull request number is not set. Please set the PR_NUMBER environment variable." + ) + + project_number = int(project_number) + pr_number = int(pr_number) + + gh_config = GHConfig( + token=token, + organization=org, + repository=repo, + project_number=project_number, + graphql_url="https://api.github.com/graphql", + rest_url=f"https://api.github.com/repos/{org}/{repo}/issues", + headers={"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json"}, + ) + project_item_handler = ProjectItemHandler(gh_config=gh_config) + + # Get PR info + pr = project_item_handler.repo.get_pull(pr_number) + + # Get the linked issues of the pull request + linked_issues = project_item_handler.get_pull_request_linked_issues(pr_number=pr_number) + print(f"Linked issues: {linked_issues}") + + target_status = "In Development" if pr.draft else "Ready For Review" + print(f"Target status: {target_status}") + for issue in linked_issues: + project_item_handler.set_issue_status(issue_number=issue["number"], status=target_status) + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/pr_issue_sync/requirements.txt b/.github/scripts/pr_issue_sync/requirements.txt new file mode 100644 index 0000000..9f191a9 --- /dev/null +++ b/.github/scripts/pr_issue_sync/requirements.txt @@ -0,0 +1,2 @@ +pydantic +pygithub \ No newline at end of file diff --git a/.github/workflows/check_pr.yml b/.github/workflows/check_pr.yml new file mode 100644 index 0000000..dddcfc6 --- /dev/null +++ b/.github/workflows/check_pr.yml @@ -0,0 +1,28 @@ +name: Check PR status for branch +on: + workflow_call: + outputs: + branch-pr: + description: The PR number if the branch is in one + value: ${{ jobs.pr.outputs.branch-pr }} + +jobs: + pr: + runs-on: "ubuntu-latest" + outputs: + branch-pr: ${{ steps.script.outputs.result }} + steps: + - uses: actions/github-script@v7 + id: script + if: github.event_name == 'push' && github.event.ref_type != 'tag' + with: + script: | + const prs = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + head: context.repo.owner + ':${{ github.ref_name }}' + }) + if (prs.data.length) { + console.log(`::notice ::Skipping CI on branch push as it is already run in PR #${prs.data[0]["number"]}`) + return prs.data[0]["number"] + } \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..afb3742 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +name: Full CI +on: + push: + pull_request: + workflow_dispatch: + inputs: + BEC_CORE_BRANCH: + description: 'Branch of BEC Core to install' + required: false + type: string + OPHYD_DEVICES_BRANCH: + description: 'Branch of Ophyd Devices to install' + required: false + type: string + +permissions: + pull-requests: write + +jobs: + check_pr_status: + uses: ./.github/workflows/check_pr.yml + + formatter: + needs: check_pr_status + if: needs.check_pr_status.outputs.branch-pr == '' + uses: ./.github/workflows/formatter.yml + + unit-test: + needs: [check_pr_status, formatter] + if: needs.check_pr_status.outputs.branch-pr == '' + uses: ./.github/workflows/pytest.yml + with: + BEC_CORE_BRANCH: ${{ inputs.BEC_CORE_BRANCH || 'main' }} + OPHYD_DEVICES_BRANCH: ${{ inputs.OPHYD_DEVICES_BRANCH || github.head_ref || github.sha}} + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + unit-test-matrix: + needs: [check_pr_status, formatter] + if: needs.check_pr_status.outputs.branch-pr == '' + uses: ./.github/workflows/pytest-matrix.yml + with: + BEC_CORE_BRANCH: ${{ inputs.BEC_CORE_BRANCH || 'main' }} + OPHYD_DEVICES_BRANCH: ${{ inputs.OPHYD_DEVICES_BRANCH || github.head_ref || github.sha}} \ No newline at end of file diff --git a/.github/workflows/formatter.yml b/.github/workflows/formatter.yml new file mode 100644 index 0000000..51c6216 --- /dev/null +++ b/.github/workflows/formatter.yml @@ -0,0 +1,61 @@ +name: Formatter and Pylint jobs +on: [workflow_call] +jobs: + + Formatter: + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Run black and isort + run: | + pip install black isort + pip install -e .[dev] + black --check --diff --color . + isort --check --diff ./ + Pylint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint pylint-exit anybadge + + - name: Run Pylint + run: | + mkdir -p ./pylint + set +e + pylint ./${{ github.event.repository.name }} --output-format=text > ./pylint/pylint.log + pylint-exit $? + set -e + + - name: Extract Pylint Score + id: score + run: | + SCORE=$(sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' ./pylint/pylint.log) + echo "score=$SCORE" >> $GITHUB_OUTPUT + + - name: Create Badge + run: | + anybadge --label=Pylint --file=./pylint/pylint.svg --value="${{ steps.score.outputs.score }}" 2=red 4=orange 8=yellow 10=green + + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + with: + name: pylint-artifacts + path: | + # ./pylint/pylint.log # not sure why this isn't working + ./pylint/pylint.svg \ No newline at end of file diff --git a/.github/workflows/pytest-matrix.yml b/.github/workflows/pytest-matrix.yml new file mode 100644 index 0000000..56c2c78 --- /dev/null +++ b/.github/workflows/pytest-matrix.yml @@ -0,0 +1,42 @@ +name: Run Pytest with different Python versions +on: + workflow_call: + inputs: + BEC_CORE_BRANCH: + description: 'Branch of BEC Core to install' + required: false + default: 'main' + type: string + OPHYD_DEVICES_BRANCH: + description: 'Branch of Ophyd Devices to install' + required: false + default: 'main' + type: string + +jobs: + pytest-matrix: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12"] + + steps: + + - name: Checkout BEC Ophyd Devices + uses: actions/checkout@v4 + with: + repository: bec-project/ophyd_devices + ref: ${{ inputs.OPHYD_DEVICES_BRANCH }} + + - name: Install Ophyd Devices and dependencies + uses: ./.github/actions/ophyd_devices_install + with: + BEC_CORE_BRANCH: ${{ inputs.BEC_CORE_BRANCH }} + OPHYD_DEVICES_BRANCH: ${{ inputs.OPHYD_DEVICES_BRANCH }} + PYTHON_VERSION: ${{ matrix.python-version }} + + - name: Run Pytest + run: | + cd ./ophyd_devices + pip install pytest pytest-random-order + pytest -v --maxfail=2 --junitxml=report.xml --random-order ./tests diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 0000000..d25e761 --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,54 @@ +name: Run Pytest with Coverage +on: + workflow_call: + inputs: + BEC_CORE_BRANCH: + description: 'Branch of BEC Core to install' + required: false + default: 'main' + type: string + OPHYD_DEVICES_BRANCH: + description: 'Branch of Ophyd Devices to install' + required: false + default: 'main' + type: string + secrets: + CODECOV_TOKEN: + required: true + + + +permissions: + pull-requests: write + +jobs: + pytest: + runs-on: ubuntu-latest + + steps: + - name: Checkout Ophyd Devices + uses: actions/checkout@v4 + with: + repository: bec-project/ophyd_devices + ref: ${{ inputs.OPHYD_DEVICES_BRANCH }} + + - name: Install Ophyd Devices and dependencies + uses: ./.github/actions/ophyd_devices_install + with: + BEC_CORE_BRANCH: ${{ inputs.BEC_CORE_BRANCH }} + OPHYD_DEVICES_BRANCH: ${{ inputs.OPHYD_DEVICES_BRANCH }} + PYTHON_VERSION: 3.11 + + - name: Run Pytest with Coverage + id: coverage + run: | + cd ./ophyd_devices + coverage run --source=./ophyd_devices --omit=*/ophyd_devices/tests/* -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests + coverage report + coverage xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + slug: bec-project/ophyd_devices \ No newline at end of file diff --git a/.github/workflows/semantic_release.yml b/.github/workflows/semantic_release.yml new file mode 100644 index 0000000..1edc111 --- /dev/null +++ b/.github/workflows/semantic_release.yml @@ -0,0 +1,95 @@ +name: Continuous Delivery + +on: + push: + branches: + - main + +# default: least privileged permissions across all jobs +permissions: + contents: read + + + +jobs: + release: + runs-on: ubuntu-latest + concurrency: + group: ${{ github.workflow }}-release-${{ github.ref_name }} + cancel-in-progress: false + + permissions: + contents: write + + steps: + # Note: We checkout the repository at the branch that triggered the workflow + # with the entire history to ensure to match PSR's release branch detection + # and history evaluation. + # However, we forcefully reset the branch to the workflow sha because it is + # possible that the branch was updated while the workflow was running. This + # prevents accidentally releasing un-evaluated changes. + - name: Setup | Checkout Repository on Release Branch + uses: actions/checkout@v4 + with: + ref: ${{ github.ref_name }} + fetch-depth: 0 + ssh-key: ${{ secrets.CI_DEPLOY_SSH_KEY }} + ssh-known-hosts: ${{ secrets.CI_DEPLOY_SSH_KNOWN_HOSTS }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Setup | Force release branch to be at workflow sha + run: | + git reset --hard ${{ github.sha }} + - name: Evaluate | Verify upstream has NOT changed + # Last chance to abort before causing an error as another PR/push was applied to + # the upstream branch while this workflow was running. This is important + # because we are committing a version change (--commit). You may omit this step + # if you have 'commit: false' in your configuration. + # + # You may consider moving this to a repo script and call it from this step instead + # of writing it in-line. + shell: bash + run: | + set +o pipefail + + UPSTREAM_BRANCH_NAME="$(git status -sb | head -n 1 | cut -d' ' -f2 | grep -E '\.{3}' | cut -d'.' -f4)" + printf '%s\n' "Upstream branch name: $UPSTREAM_BRANCH_NAME" + + set -o pipefail + + if [ -z "$UPSTREAM_BRANCH_NAME" ]; then + printf >&2 '%s\n' "::error::Unable to determine upstream branch name!" + exit 1 + fi + + git fetch "${UPSTREAM_BRANCH_NAME%%/*}" + + if ! UPSTREAM_SHA="$(git rev-parse "$UPSTREAM_BRANCH_NAME")"; then + printf >&2 '%s\n' "::error::Unable to determine upstream branch sha!" + exit 1 + fi + + HEAD_SHA="$(git rev-parse HEAD)" + + if [ "$HEAD_SHA" != "$UPSTREAM_SHA" ]; then + printf >&2 '%s\n' "[HEAD SHA] $HEAD_SHA != $UPSTREAM_SHA [UPSTREAM SHA]" + printf >&2 '%s\n' "::error::Upstream has changed, aborting release..." + exit 1 + fi + + printf '%s\n' "Verified upstream branch has not changed, continuing with release..." + + - name: Semantic Version Release + id: release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + pip install python-semantic-release==9.* wheel build twine + semantic-release -vv version + if [ ! -d dist ]; then echo No release will be made; exit 0; fi + twine upload dist/* -u __token__ -p ${{ secrets.CI_PYPI_TOKEN }} --skip-existing + semantic-release publish diff --git a/.github/workflows/sync-issues-pr.yml b/.github/workflows/sync-issues-pr.yml new file mode 100644 index 0000000..a35cb9a --- /dev/null +++ b/.github/workflows/sync-issues-pr.yml @@ -0,0 +1,40 @@ +name: Sync PR to Project + +on: + pull_request: + types: [opened, edited, ready_for_review, converted_to_draft, reopened, synchronize] + +jobs: + sync-project: + runs-on: ubuntu-latest + + permissions: + issues: write + pull-requests: read + contents: read + + env: + PROJECT_NUMBER: 3 # BEC Project + ORG: 'bec-project' + REPO: 'ophyd_devices' + TOKEN: ${{ secrets.ADD_ISSUE_TO_PROJECT }} + PR_NUMBER: ${{ github.event.pull_request.number }} + + steps: + - name: Set up python environment + uses: actions/setup-python@v4 + with: + python-version: 3.11 + + - name: Checkout repo + uses: actions/checkout@v4 + with: + repository: ${{ github.repository }} + ref: ${{ github.event.pull_request.head.ref }} + + - name: Install dependencies + run: | + pip install -r ./.github/scripts/pr_issue_sync/requirements.txt + - name: Sync PR to Project + run: | + python ./.github/scripts/pr_issue_sync/pr_issue_sync.py \ No newline at end of file