Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
07d5c29
fix(ai): preserve invalid workspace config failures
ice-black-tea Sep 21, 2026
1283546
fix(release): verify candidate artifacts before publishing
ice-black-tea Sep 21, 2026
83d9a82
fix(ai): declare cli runtime dependency
ice-black-tea Sep 21, 2026
f586b7e
refactor(release): keep artifact smoke verification minimal
ice-black-tea Sep 22, 2026
b9e650f
fix(release): make artifact smoke checks executable
ice-black-tea Sep 22, 2026
10f37d9
fix(release): parse candidate dependency names
ice-black-tea Sep 22, 2026
1bb6fb3
refactor(release): let pip resolve candidate dependencies
ice-black-tea Sep 22, 2026
cf1989f
fix(ai): declare yaml runtime dependency
ice-black-tea Sep 22, 2026
b6b4cc1
fix(release): freeze refs before publication
ice-black-tea Sep 22, 2026
c552be1
fix(release): verify resumed package uploads
ice-black-tea Sep 22, 2026
ab9cecb
refactor(release): discover published projects
ice-black-tea Sep 22, 2026
dad467d
fix(release): restore publish workflow integrity
ice-black-tea Sep 22, 2026
8fbf3c0
fix(release): archive exact retry artifacts
ice-black-tea Sep 22, 2026
190005c
fix(release): serialize version publication
ice-black-tea Sep 22, 2026
1901551
docs: refresh root release usage
ice-black-tea Sep 22, 2026
2174160
docs(core): refresh package usage
ice-black-tea Sep 22, 2026
2f37a54
docs(common): document runtime requirement
ice-black-tea Sep 22, 2026
b02ad46
docs(mobile): refresh install and aliases
ice-black-tea Sep 22, 2026
22a78b8
docs(cntr): refresh runtime requirements
ice-black-tea Sep 22, 2026
3938230
docs(ai): document install and cli surface
ice-black-tea Sep 22, 2026
3a65cfc
docs: fix ios command alias
ice-black-tea Sep 22, 2026
61fea89
docs(ai): clarify acp dependency
ice-black-tea Sep 22, 2026
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
258 changes: 226 additions & 32 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,61 +22,105 @@ env:
PIP_DISABLE_PIP_VERSION_CHECK: "1"
PYTHONUNBUFFERED: "1"

concurrency:
group: python-publish-${{ inputs.version }}
cancel-in-progress: false

jobs:
validate:
runs-on: ubuntu-latest
outputs:
prerelease: ${{ steps.version.outputs.prerelease }}
release_ref: ${{ steps.version.outputs.release_ref }}
resume: ${{ steps.version.outputs.resume }}
steps:
- uses: actions/checkout@v7
- name: Validate version
with:
fetch-depth: 0
- name: Validate release state
id: version
env:
VERSION: ${{ inputs.version }}
run: |
VERSION="${{ inputs.version }}"
if ! [[ "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(rc[0-9]+)?$ ]]; then
echo "::error::Invalid version format: $VERSION (expected vX.Y.Z or vX.Y.ZrcN, e.g. v1.2.3 or v1.2.3rc0)"
exit 1
fi
if git ls-remote --exit-code --tags origin "refs/tags/$VERSION" >/dev/null 2>&1; then
echo "::error::Tag $VERSION already exists"
if [[ "$GITHUB_REF" != "refs/heads/master" ]]; then
echo "::error::Release workflow must be dispatched from master"
exit 1
fi
if [[ "$VERSION" =~ rc[0-9]+$ ]]; then
echo "prerelease=true" >> "$GITHUB_OUTPUT"
else
echo "prerelease=false" >> "$GITHUB_OUTPUT"
fi

git fetch origin master --tags

if git show-ref --verify --quiet "refs/tags/$VERSION"; then
RELEASE_COMMIT="$(git rev-list -n 1 "$VERSION")"
RELEASE_SUBJECT="$(git log -1 --format=%s "$RELEASE_COMMIT")"
if [[ "$RELEASE_SUBJECT" != "build(release): prepare $VERSION" ]]; then
echo "::error::Existing tag $VERSION is not owned by the release workflow"
exit 1
fi
if ! git merge-base --is-ancestor "$RELEASE_COMMIT" origin/master; then
echo "::error::Existing tag $VERSION is not reachable from master"
exit 1
fi
echo "resume=true" >> "$GITHUB_OUTPUT"
echo "release_ref=$VERSION" >> "$GITHUB_OUTPUT"
else
if [[ "$(git rev-parse origin/master)" != "$GITHUB_SHA" ]]; then
echo "::error::master moved after this release run was dispatched; start a new run"
exit 1
fi
echo "resume=false" >> "$GITHUB_OUTPUT"
echo "release_ref=$GITHUB_SHA" >> "$GITHUB_OUTPUT"
fi

checks:
needs: validate
if: ${{ needs.validate.outputs.resume != 'true' }}
uses: ./.github/workflows/python-check.yml

deploy:
needs:
- validate
- checks
if: ${{ always() && needs.validate.result == 'success' && (needs.validate.outputs.resume == 'true' || needs.checks.result == 'success') }}
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.validate.outputs.release_ref }}
fetch-depth: 0

# build frida.min.js
# build generated source only while preparing a new release commit
- name: Set up Node
if: ${{ !env.ACT }}
if: ${{ needs.validate.outputs.resume != 'true' && !env.ACT }}
uses: actions/setup-node@v7
with:
node-version: 20
- name: Build frida scripts
if: ${{ !env.ACT }}
if: ${{ needs.validate.outputs.resume != 'true' && !env.ACT }}
run: cd linktools-mobile/agents/frida && npm install && npm run build

# build android-tools.apk
- name: Set up JDK 17
if: ${{ !env.ACT }}
if: ${{ needs.validate.outputs.resume != 'true' && !env.ACT }}
uses: actions/setup-java@v6
with:
distribution: temurin
java-version: 17
- name: Setup Android SDK
if: ${{ !env.ACT }}
if: ${{ needs.validate.outputs.resume != 'true' && !env.ACT }}
uses: android-actions/setup-android@v4
- name: Build android tools
if: ${{ !env.ACT }}
if: ${{ needs.validate.outputs.resume != 'true' && !env.ACT }}
run: cd linktools-mobile/agents/android && ./gradlew --no-daemon :tools:buildTools

# build python package
- name: Set up Python
uses: actions/setup-python@v7
with:
Expand All @@ -89,39 +133,189 @@ jobs:
run: python manage.py install --editable --quiet
- name: Clean package
run: python manage.py clean

# a completed draft archive is the exact recovery source for interrupted uploads
- name: Restore archived release artifacts
id: restore
if: ${{ needs.validate.outputs.resume == 'true' && !env.ACT }}
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ inputs.version }}
run: |
echo "restored=false" >> "$GITHUB_OUTPUT"
rm -f release-sha256sums.txt

if ! IS_DRAFT="$(gh release view "$VERSION" --json isDraft --jq '.isDraft' 2>/dev/null)"; then
exit 0
fi
if ! gh release download "$VERSION" --pattern 'release-sha256sums.txt' --output release-sha256sums.txt >/dev/null 2>&1; then
exit 0
fi

mkdir -p dist
gh release download "$VERSION" --pattern '*.whl' --pattern '*.tar.gz' --dir dist
(
cd dist
sha256sum --check ../release-sha256sums.txt
)
echo "restored=true" >> "$GITHUB_OUTPUT"

- name: Build package
run: VERSION=${{ inputs.version }} RELEASE=true python manage.py build
if: ${{ needs.validate.outputs.resume != 'true' || steps.restore.outputs.restored != 'true' }}
env:
RESUME: ${{ needs.validate.outputs.resume }}
VERSION: ${{ inputs.version }}
run: |
if [[ "$RESUME" == "true" ]]; then
RELEASE=true python manage.py build
else
VERSION="$VERSION" RELEASE=true python manage.py build
fi

- name: Verify package
run: VERSION=${{ inputs.version }} RELEASE=true python manage.py verify

# publish python package
- name: Publish package
if: ${{ !env.ACT }}
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
packages_dir: dist/

# commit and push artifacts (fast-forward, no tag yet)
- name: Commit files
if: ${{ !env.ACT }}
# freeze source and tag before any external package publication
- name: Create release commit and tag
if: ${{ needs.validate.outputs.resume != 'true' && !env.ACT }}
env:
VERSION: ${{ inputs.version }}
run: |
git config user.name github-actions[bot]
git config user.email github-actions[bot]@users.noreply.github.com

git add linktools*/linktools.yml \
linktools-mobile/src/linktools/assets/frida.js \
linktools-mobile/src/linktools/assets/frida-*.js \
linktools-mobile/src/linktools/assets/android-tools.*
git commit -m "Auto commit artifacts (${{ inputs.version }})"
git push origin HEAD:master

# tag and release only after artifacts are pushed, so they match exactly
- name: Create tag and release
if ! git diff --quiet; then
echo "::error::Release preparation left unstaged tracked changes"
git status --short
exit 1
fi

git commit --allow-empty -m "build(release): prepare $VERSION"
git tag "$VERSION"

git fetch origin master
if [[ "$(git rev-parse origin/master)" != "$GITHUB_SHA" ]]; then
echo "::error::master moved while preparing $VERSION; no release refs were pushed"
exit 1
fi

git push --atomic origin HEAD:master "refs/tags/$VERSION"
git checkout --detach "$VERSION"

- name: Verify fixed release ref
if: ${{ !env.ACT }}
env:
VERSION: ${{ inputs.version }}
run: |
TAG_COMMIT="$(git rev-list -n 1 "$VERSION")"
if [[ "$(git rev-parse HEAD)" != "$TAG_COMMIT" ]]; then
echo "::error::Release build is not on the fixed tag $VERSION"
exit 1
fi
if ! git diff --quiet || ! git diff --cached --quiet; then
echo "::error::Release build changed tracked source after the tag was fixed"
git status --short
exit 1
fi

# archive the verified bytes before PyPI so retries never depend on reproducible builds
- name: Archive release artifacts
if: ${{ steps.restore.outputs.restored != 'true' && !env.ACT }}
env:
GH_TOKEN: ${{ github.token }}
PRERELEASE: ${{ needs.validate.outputs.prerelease }}
VERSION: ${{ inputs.version }}
run: |
git tag ${{ inputs.version }}
git push origin ${{ inputs.version }}
gh release create ${{ inputs.version }} --generate-notes dist/*
if IS_DRAFT="$(gh release view "$VERSION" --json isDraft --jq '.isDraft' 2>/dev/null)"; then
if [[ "$IS_DRAFT" != "true" ]]; then
echo "::error::Published release $VERSION has no complete recovery archive"
exit 1
fi
else
if [[ "$PRERELEASE" == "true" ]]; then
gh release create "$VERSION" --draft --generate-notes --prerelease --latest=false
else
gh release create "$VERSION" --draft --generate-notes
fi
fi

(
cd dist
sha256sum * | sort > ../release-sha256sums.txt
)
gh release upload "$VERSION" dist/* --clobber
gh release upload "$VERSION" release-sha256sums.txt --clobber

# any PyPI files from an interrupted upload must be byte-identical to the archived candidate
- name: Verify resumable PyPI state
if: ${{ needs.validate.outputs.resume == 'true' && !env.ACT }}
env:
VERSION: ${{ inputs.version }}
run: |
PYPI_VERSION="${VERSION#v}"
mapfile -t PROJECTS < <(python manage.py modules | jq -r '.[]')
for PROJECT in "${PROJECTS[@]}"; do
RESPONSE="$(mktemp)"
STATUS="$(curl --silent --show-error --location \
--output "$RESPONSE" --write-out '%{http_code}' \
"https://pypi.org/pypi/$PROJECT/$PYPI_VERSION/json")"
if [[ "$STATUS" == "404" ]]; then
rm -f "$RESPONSE"
continue
fi
if [[ "$STATUS" != "200" ]]; then
echo "::error::PyPI metadata request failed for $PROJECT $PYPI_VERSION: HTTP $STATUS"
rm -f "$RESPONSE"
exit 1
fi

while IFS='|' read -r FILENAME EXPECTED_SHA256; do
[[ -n "$FILENAME" ]] || continue
LOCAL_PATH="dist/$FILENAME"
if [[ ! -f "$LOCAL_PATH" ]]; then
echo "::error::PyPI contains unexpected artifact $PROJECT/$PYPI_VERSION/$FILENAME"
rm -f "$RESPONSE"
exit 1
fi
ACTUAL_SHA256="$(sha256sum "$LOCAL_PATH" | cut -d' ' -f1)"
if [[ "$ACTUAL_SHA256" != "$EXPECTED_SHA256" ]]; then
echo "::error::PyPI artifact differs from archived candidate: $FILENAME"
rm -f "$RESPONSE"
exit 1
fi
done < <(jq -r '.urls[] | "\(.filename)|\(.digests.sha256)"' "$RESPONSE")
rm -f "$RESPONSE"
done

- name: Publish package
if: ${{ !env.ACT }}
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
packages_dir: dist/
skip-existing: ${{ needs.validate.outputs.resume }}

- name: Publish GitHub release
if: ${{ !env.ACT }}
env:
GH_TOKEN: ${{ github.token }}
PRERELEASE: ${{ needs.validate.outputs.prerelease }}
VERSION: ${{ inputs.version }}
run: |
IS_DRAFT="$(gh release view "$VERSION" --json isDraft --jq '.isDraft')"
if [[ "$IS_DRAFT" != "true" ]]; then
echo "GitHub release $VERSION is already published"
exit 0
fi

if [[ "$PRERELEASE" == "true" ]]; then
gh release edit "$VERSION" --draft=false --prerelease --latest=false
else
gh release edit "$VERSION" --draft=false --prerelease=false
fi
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@ Linktools 是一套面向移动安全研究、逆向分析、合规检测工具
| [linktools-common](linktools-common/) | 通用工具:`ct-env`、`ct-grep`、`ct-tools` | [README](linktools-common/README.md) |
| [linktools-mobile](linktools-mobile/) | 移动设备:Android(`at-*`)和 iOS(`it-*`)工具 | [README](linktools-mobile/README.md) |
| [linktools-cntr](linktools-cntr/) | 容器管理:Docker/Compose 部署工具(`ct-cntr`) | [README](linktools-cntr/README.md) |
| [linktools-ai](linktools-ai/) | AI agent 运行时:session/execution/swarm,基于 pydantic-ai(纯库,无 CLI) | [README](linktools-ai/README.md) |
| [linktools-ai](linktools-ai/) | AI agent 运行时:session/execution/swarm,基于 pydantic-ai,并提供本地调试 CLI | [README](linktools-ai/README.md) |

## 快速开始

### 依赖项

Python & pip(3.6 及以上):<https://www.python.org/downloads/>
Python & pip:除 `linktools-ai` 外的子包支持 Python 3.6 及以上;`linktools-ai` 需要 Python 3.10 及以上。<https://www.python.org/downloads/>

### 安装

```bash
# 安装方式一:安装所有包
# 安装方式一:安装 linktools 的全部可选能力(包含 common/mobile/cntr,不包含独立的 linktools-ai)
python3 -m pip install -U "linktools[all]"

# 安装方式二:按需安装子包
Expand All @@ -36,7 +36,7 @@ python3 -m pip install --ignore-installed \
"linktools-common@ git+https://github.com/linktools-toolkit/linktools.git@master#subdirectory=linktools-common" \
"linktools-mobile@ git+https://github.com/linktools-toolkit/linktools.git@master#subdirectory=linktools-mobile" \
"linktools-cntr@ git+https://github.com/linktools-toolkit/linktools.git@master#subdirectory=linktools-cntr" \
"linktools-ai@ git+https://github.com/linktools-toolkit/linktools.git@master#subdirectory=linktools-ai"
"linktools-ai[sqlite] @ git+https://github.com/linktools-toolkit/linktools.git@master#subdirectory=linktools-ai"
```

### 配置 alias(推荐)
Expand All @@ -52,7 +52,7 @@ eval "$(ct-env --silent java 17.0.11 --shell bash)"

# 常用 alias
alias adb="at-adb"
alias sib="it-sib"
alias sib="it-ios"
alias pidcat="at-pidcat"

alias apktool="ct-tools apktool"
Expand All @@ -66,7 +66,7 @@ alias jadx="ct-tools --set version=1.5.0 jadx-gui"
$ python3 -m linktools
___ __ __ __
/ (_)___ / /__/ /_____ ____ / /____
/ / / __ \/ //_/ __/ __ \/ __ \/ / ___/ linktools toolkit (v0.9.0)
/ / / __ \/ //_/ __/ __ \/ __ \/ / ___/ linktools toolkit (v0.10.0)
/ / / / / / ,< / /_/ /_/ / /_/ / (__ ) by: Hu Ji <669898595@qq.com>
/_/_/_/ /_/_/|_|\__/\____/\____/_/____/
📎 All commands
Expand Down
Loading
Loading