diff --git a/.github/scripts/check_release_version.py b/.github/scripts/check_release_version.py new file mode 100644 index 0000000..13f4c9b --- /dev/null +++ b/.github/scripts/check_release_version.py @@ -0,0 +1,26 @@ +# /// script +# dependencies = ["packaging"] +# /// +"""Validate RELEASE_VERSION against RELEASE_TAGS.""" + +import contextlib +import os +import sys + +from packaging.version import InvalidVersion, Version + +release = Version(os.environ["RELEASE_VERSION"]) +if release.local is not None: + sys.exit(f"v{release} is a local version and cannot be published to PyPI") + +versions = [] +for tag in os.environ["RELEASE_TAGS"].split(): + with contextlib.suppress(InvalidVersion): + versions.append(Version(tag)) + +if release in versions: + sys.exit(f"v{release} has already been released") + +latest = max(versions, default=None) +if latest is not None and release < latest: + sys.exit(f"v{release} is not newer than the latest release, v{latest}") diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 201265b..bdd4f55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: branches: - main pull_request: + merge_group: permissions: {} diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 0000000..f3a947f --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,93 @@ +name: prepare release + +on: + workflow_dispatch: + inputs: + version: + description: Version to release; leave blank to bump the minor version + required: false + type: string + +permissions: {} + +concurrency: + group: prepare-release + cancel-in-progress: false + +jobs: + prepare: + name: Create release branch + runs-on: ubuntu-latest + permissions: + contents: write + env: + REQUESTED_VERSION: ${{ inputs.version }} + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: main + + - name: Install uv + uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 + with: + enable-cache: false + + - name: Bump version + run: | + if test -n "$REQUESTED_VERSION"; then + uv version "$REQUESTED_VERSION" --no-sync + else + uv version --bump minor --no-sync + fi + + release_version=$(uv version --short) + release_tags=$(git ls-remote --tags origin 'v*' | cut -f2 | sed -e 's|^refs/tags/||' -e '/\^{}$/d') + RELEASE_VERSION="$release_version" RELEASE_TAGS="$release_tags" uv run .github/scripts/check_release_version.py + + if git diff --quiet -- pyproject.toml uv.lock; then + echo "v$release_version does not change the version files" >&2 + exit 1 + fi + + echo "RELEASE_VERSION=$release_version" >> "$GITHUB_ENV" + + - name: Create release branch + id: commit + env: + GH_TOKEN: ${{ github.token }} + run: | + release_branch="release/v$RELEASE_VERSION" + + if git ls-remote --exit-code origin "refs/heads/$release_branch" >/dev/null; then + echo "$release_branch already exists; delete it before preparing this release again" >&2 + exit 1 + fi + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add pyproject.toml uv.lock + git commit -m "Prepare release v$RELEASE_VERSION" + + release_commit=$(git rev-parse HEAD) + gh auth setup-git + git push origin "HEAD:refs/heads/$release_branch" + + echo "branch=$release_branch" >> "$GITHUB_OUTPUT" + echo "commit=$release_commit" >> "$GITHUB_OUTPUT" + + - name: Show pull request link + env: + RELEASE_BRANCH: ${{ steps.commit.outputs.branch }} + RELEASE_COMMIT: ${{ steps.commit.outputs.commit }} + run: | + { + echo "## Release v$RELEASE_VERSION is ready" + echo + echo "Commit: \`$RELEASE_COMMIT\`" + echo + echo "[Open the release pull request](https://github.com/$GITHUB_REPOSITORY/compare/main...$RELEASE_BRANCH?expand=1)" + echo + echo "Merging this pull request will publish the release automatically." + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b4a82ef --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,161 @@ +name: publish release + +on: + pull_request: + types: + - closed + branches: + - main + +permissions: {} + +# Serialize release PRs in FIFO order without letting no-op runs block them. +concurrency: + group: ${{ github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/') && 'release' || format('release-noop-{0}', github.run_id) }} + cancel-in-progress: false + queue: max + +jobs: + build: + name: Build and tag distributions + if: >- + github.event.pull_request.merged == true && + github.event.pull_request.base.ref == 'main' && + github.event.pull_request.head.repo.full_name == github.repository && + startsWith(github.event.pull_request.head.ref, 'release/') + runs-on: ubuntu-latest + outputs: + release-version: ${{ steps.version.outputs.release-version }} + permissions: + contents: write + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + ref: ${{ github.event.pull_request.merge_commit_sha }} + + - name: Install uv + uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 + with: + enable-cache: false + + - name: Read version + id: version + env: + HEAD_REF: ${{ github.event.pull_request.head.ref }} + run: | + release_version=$(uv version --short) + release_tag="v$release_version" + + if test "$HEAD_REF" != "release/$release_tag"; then + echo "$HEAD_REF does not match the version being released, $release_tag" >&2 + exit 1 + fi + + # Skip version-order validation when rerunning this tagged commit. + if test "$(git rev-parse --verify --quiet "refs/tags/$release_tag^{commit}" || true)" != "$(git rev-parse HEAD)"; then + release_tags=$(git tag --list 'v*') + RELEASE_VERSION="$release_version" RELEASE_TAGS="$release_tags" uv run .github/scripts/check_release_version.py + fi + + echo "RELEASE_VERSION=$release_version" >> "$GITHUB_ENV" + echo "release-version=$release_version" >> "$GITHUB_OUTPUT" + + - name: Build distributions + run: uv build + + # Let build-job reruns replace the previous artifact. + - name: Upload distributions + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: distributions + path: dist/ + overwrite: true + + # Preserve a tag on this commit; a plain push rejects remote conflicts. + - name: Push release tag + env: + GH_TOKEN: ${{ github.token }} + run: | + tag_commit=$(git rev-parse --verify --quiet "refs/tags/v$RELEASE_VERSION^{commit}" || true) + if test "$tag_commit" != "$(git rev-parse HEAD)"; then + git tag -f "v$RELEASE_VERSION" + fi + gh auth setup-git + git push origin "refs/tags/v$RELEASE_VERSION" + + generate-provenance: + name: Generate build provenance + runs-on: ubuntu-latest + needs: build + permissions: + id-token: write + attestations: write + steps: + - name: Download distributions + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: distributions + path: dist/ + + - name: Attest distributions + uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 + with: + subject-path: dist/* + + release: + name: Create GitHub release + runs-on: ubuntu-latest + needs: + - build + - generate-provenance + permissions: + contents: write + steps: + - name: Download distributions + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: distributions + path: dist/ + + - name: Create GitHub release + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + RELEASE_VERSION: ${{ needs.build.outputs.release-version }} + run: | + # Keep existing release assets unchanged on reruns. + if ! gh release view "v$RELEASE_VERSION" >/dev/null 2>&1; then + gh release create "v$RELEASE_VERSION" dist/* --generate-notes --verify-tag + fi + + publish: + name: Publish distributions to PyPI + runs-on: ubuntu-latest + needs: + - build + - generate-provenance + - release + environment: + name: pypi + url: https://pypi.org/p/idac + permissions: + contents: read + id-token: write + steps: + # Reuse release assets so partial PyPI uploads resume with identical bytes. + - name: Download release assets + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + RELEASE_VERSION: ${{ needs.build.outputs.release-version }} + run: gh release download "v$RELEASE_VERSION" --dir dist/ + + - name: Publish distributions + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 + with: + attestations: true + # Ignore files already published by a partial run. + skip-existing: true diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index a6c7692..59ecf5b 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -5,6 +5,7 @@ on: branches: ["main"] pull_request: branches: ["**"] + merge_group: permissions: {} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7a20b31 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Trail of Bits + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index e3aa9e8..4feb594 100644 --- a/README.md +++ b/README.md @@ -90,17 +90,21 @@ The same command works against a live GUI session — drop `-c` and `idac` auto- ## Quick start -Clone and install the CLI, then wire up the GUI plugin and agent skill: +Install the CLI from [PyPI](https://pypi.org/project/idac/), then wire up the GUI plugin and agent skill: ```bash -git clone https://github.com/trailofbits/idac.git -cd idac -uv tool install . # installs the `idac` command on your PATH +uv tool install idac # installs the `idac` command on your PATH idac doctor # verify IDA install, license, and bridge idac misc plugin install # GUI bridge plugin idac misc skill install # Claude Code + Codex skill ``` +To install the latest development version straight from git instead: + +```bash +uv tool install git+https://github.com/trailofbits/idac.git +``` + Talk to a live GUI session: ```bash @@ -270,11 +274,19 @@ This installs into both `~/.claude/skills/idac` and `~/.codex/skills/idac`; both ## Development ```bash -uv sync +git clone https://github.com/trailofbits/idac.git +cd idac +uv sync # project venv with an editable install; run via `uv run idac ...` make test # run tests make check # format + lint + test + audit ``` +To put an `idac` on your PATH that tracks your checkout, install it as editable: + +```bash +uv tool install -e . +``` + See [docs/development.md](docs/development.md) for fixture regeneration, live GUI tests, and local tooling details. ## Credits diff --git a/pyproject.toml b/pyproject.toml index 9b42878..5234f09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,6 +3,8 @@ name = "idac" version = "0.18.0" description = "Agent-friendly CLI for IDA with GUI and idalib backends" readme = "README.md" +license = "Apache-2.0" +license-files = ["LICENSE"] authors = [ { name = "Jay Little / Trail of Bits", email = "jay@trailofbits.com" } ]