name: Build and Release on: workflow_dispatch: inputs: version: description: '版本号 (留空则自动生成 v-)' required: false default: '' prerelease: description: '标记为预发布 (Pre-release)' required: false default: 'false' type: choice options: - 'false' - 'true' draft: description: '以草稿方式创建 Release (不对外公开)' required: false default: 'false' type: choice options: - 'false' - 'true' skip_tests: description: '跳过单元测试 (紧急构建时使用)' required: false default: 'false' type: choice options: - 'false' - 'true' jobs: build-release: runs-on: ubuntu-latest env: # 仓库使用 SHA-256 对象格式,actions/checkout 内部 `git init` 需在此模式下运行,否则 fetch 会报 # `mismatched algorithms: client sha1; server sha256`。 GIT_DEFAULT_HASH: sha256 steps: - name: Checkout (full history) uses: actions/checkout@v4 with: fetch-depth: 0 - name: Network diagnostics run: | set +e echo "=== DNS ===" cat /etc/resolv.conf 2>/dev/null | head -5 || true echo "=== Route ===" ip route 2>/dev/null | head -3 || true echo "=== Egress probe (5s connect timeout) ===" for url in \ https://github.com \ https://objects.githubusercontent.com \ https://mirrors.aliyun.com \ http://archive.ubuntu.com \ http://security.ubuntu.com \ ; do code=$(curl -sS -o /dev/null -w '%{http_code}' -m 5 --connect-timeout 5 "$url" || echo TIMEOUT) printf ' %-45s -> %s\n' "$url" "$code" done - name: Ensure required tools (shellcheck via GitHub release) run: | set -euo pipefail # 镜像 gitea/runner-images:ubuntu-latest 已自带 curl / jq / tar / sha256sum。 # 唯一通常缺失的是 shellcheck,从 GitHub releases 拉静态二进制即可,不走 apt。 for tool in curl jq tar sha256sum; do command -v "$tool" >/dev/null || { echo "::error::基础工具 $tool 不在 PATH 中,镜像异常。请更换 runner 镜像。" >&2 exit 1 } done if command -v shellcheck >/dev/null 2>&1; then echo "shellcheck 已就绪:$(shellcheck --version | awk '/^version:/{print $2}')" exit 0 fi SC_VER=v0.10.0 case "$(uname -m)" in x86_64) SC_ARCH=x86_64 ;; aarch64) SC_ARCH=aarch64 ;; *) echo "::error::不支持的架构: $(uname -m)" >&2; exit 1 ;; esac URL="https://github.com/koalaman/shellcheck/releases/download/${SC_VER}/shellcheck-${SC_VER}.linux.${SC_ARCH}.tar.xz" echo "下载 $URL" curl -fsSL --retry 3 --connect-timeout 15 -o /tmp/shellcheck.tar.xz "$URL" tar -xJf /tmp/shellcheck.tar.xz -C /tmp if [[ $EUID -ne 0 ]] && command -v sudo >/dev/null 2>&1; then SUDO=sudo else SUDO= fi $SUDO install -m 0755 "/tmp/shellcheck-${SC_VER}/shellcheck" /usr/local/bin/shellcheck shellcheck --version | awk '/^version:/{print "shellcheck 已安装:" $2}' - name: Run shellcheck run: | set -euo pipefail shellcheck iptables-forward.sh install.sh lib/*.sh tests/run_all.sh - name: Run unit tests if: ${{ inputs.skip_tests != 'true' }} run: | set -euo pipefail bash tests/run_all.sh --skip-integration - name: Compute version metadata id: ver run: | set -euo pipefail SHORT_SHA=$(git rev-parse --short=7 HEAD) FULL_SHA=$(git rev-parse HEAD) DATE=$(date -u +%Y.%m.%d) BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ) if [[ -n "${{ inputs.version }}" ]]; then VERSION="${{ inputs.version }}" else VERSION="v${DATE}-${SHORT_SHA}" fi TAG="${VERSION}" TITLE="IPTables 端口转发管理工具 ${VERSION}" ARTIFACT_BASE="iptables-forward-${VERSION}" { echo "version=${VERSION}" echo "tag=${TAG}" echo "title=${TITLE}" echo "artifact_base=${ARTIFACT_BASE}" echo "short_sha=${SHORT_SHA}" echo "full_sha=${FULL_SHA}" echo "build_time=${BUILD_TIME}" } >> "$GITHUB_OUTPUT" echo "准备构建: ${VERSION}" - name: Ensure tag does not already exist run: | set -euo pipefail TAG='${{ steps.ver.outputs.tag }}' if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then echo "::error::Tag ${TAG} 已存在于远端。请手动传入新的 version 输入。" exit 1 fi - name: Build release tarball id: build run: | set -euo pipefail NAME='${{ steps.ver.outputs.artifact_base }}' mkdir -p "dist/${NAME}" cp iptables-forward.sh install.sh README.md "dist/${NAME}/" cp -r lib "dist/${NAME}/" [[ -f LICENSE ]] && cp LICENSE "dist/${NAME}/" || true chmod 755 "dist/${NAME}/iptables-forward.sh" "dist/${NAME}/install.sh" tar -czf "dist/${NAME}.tar.gz" -C dist "${NAME}" ( cd dist && sha256sum "${NAME}.tar.gz" > "${NAME}.tar.gz.sha256" ) ls -la dist/ - name: Generate changelog id: changelog run: | set -euo pipefail PREV_TAG=$(git tag --list 'v*' --sort=-creatordate | head -n 1 || true) { echo "## 构建信息" echo "" echo "- **版本**: \`${{ steps.ver.outputs.version }}\`" echo "- **提交**: \`${{ steps.ver.outputs.full_sha }}\`" echo "- **构建时间 (UTC)**: ${{ steps.ver.outputs.build_time }}" echo "- **触发者**: @${{ github.actor }}" echo "" echo "## 变更记录" echo "" if [[ -n "${PREV_TAG}" ]]; then echo "自 \`${PREV_TAG}\` 以来的提交:" echo "" git log "${PREV_TAG}..HEAD" --pretty='- %s (`%h`)' --no-merges || true else echo "首次发布,近 30 条提交:" echo "" git log -30 --pretty='- %s (`%h`)' --no-merges fi echo "" echo "## 安装" echo "" echo '```bash' echo "# 下载" echo "wget ${{ github.server_url }}/${{ github.repository }}/releases/download/${{ steps.ver.outputs.tag }}/${{ steps.ver.outputs.artifact_base }}.tar.gz" echo "wget ${{ github.server_url }}/${{ github.repository }}/releases/download/${{ steps.ver.outputs.tag }}/${{ steps.ver.outputs.artifact_base }}.tar.gz.sha256" echo "" echo "# 校验" echo "sha256sum -c ${{ steps.ver.outputs.artifact_base }}.tar.gz.sha256" echo "" echo "# 解压并安装 (可选)" echo "tar -xzf ${{ steps.ver.outputs.artifact_base }}.tar.gz" echo "cd ${{ steps.ver.outputs.artifact_base }}" echo "sudo ./install.sh # 链接到 /usr/local/bin/iptables-forward" echo "" echo "# 直接运行" echo "sudo ./iptables-forward.sh" echo '```' } > dist/RELEASE_NOTES.md cat dist/RELEASE_NOTES.md - name: Create tag and Release via Gitea API env: GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITEA_URL: ${{ github.server_url }} REPO: ${{ github.repository }} TAG: ${{ steps.ver.outputs.tag }} TITLE: ${{ steps.ver.outputs.title }} SHA: ${{ steps.ver.outputs.full_sha }} ARTIFACT_BASE: ${{ steps.ver.outputs.artifact_base }} PRERELEASE: ${{ inputs.prerelease }} DRAFT: ${{ inputs.draft }} run: | set -euo pipefail BODY=$(jq -Rs . < dist/RELEASE_NOTES.md) PAYLOAD=$(jq -nc \ --arg tag "$TAG" \ --arg sha "$SHA" \ --arg name "$TITLE" \ --argjson body "$BODY" \ --argjson prerelease "$PRERELEASE" \ --argjson draft "$DRAFT" \ '{tag_name: $tag, target_commitish: $sha, name: $name, body: $body, prerelease: $prerelease, draft: $draft}') echo "Creating release: $TAG" RESP=$(curl -sSfL -X POST \ -H "Authorization: token ${GITEA_TOKEN}" \ -H "Content-Type: application/json" \ -d "${PAYLOAD}" \ "${GITEA_URL}/api/v1/repos/${REPO}/releases") RELEASE_ID=$(echo "$RESP" | jq -r '.id') if [[ -z "${RELEASE_ID}" || "${RELEASE_ID}" == "null" ]]; then echo "::error::创建 Release 失败。响应: ${RESP}" exit 1 fi echo "Release ID: ${RELEASE_ID}" for asset in "dist/${ARTIFACT_BASE}.tar.gz" "dist/${ARTIFACT_BASE}.tar.gz.sha256"; do fname=$(basename "$asset") echo "Uploading $fname" curl -sSfL -X POST \ -H "Authorization: token ${GITEA_TOKEN}" \ -F "attachment=@${asset};filename=${fname}" \ "${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${fname}" \ > /dev/null done echo "完成: ${GITEA_URL}/${REPO}/releases/tag/${TAG}" - name: Upload artifacts to workflow run (备份) uses: actions/upload-artifact@v3 with: name: ${{ steps.ver.outputs.artifact_base }} path: | dist/${{ steps.ver.outputs.artifact_base }}.tar.gz dist/${{ steps.ver.outputs.artifact_base }}.tar.gz.sha256 dist/RELEASE_NOTES.md retention-days: 30