diff --git a/.github/actions/setup-docker/action.yml b/.github/actions/setup-docker/action.yml new file mode 100644 index 0000000..846efd4 --- /dev/null +++ b/.github/actions/setup-docker/action.yml @@ -0,0 +1,23 @@ +name: "Set up Docker" +description: >- + Set up QEMU + Docker Buildx and authenticate to Docker Hub for multi-arch + image builds. Centralizes the QEMU/Buildx/login trio used by release, + preview, and stable workflows. + +inputs: + dockerhub-username: + description: "Docker Hub username (pass from secrets)" + required: true + dockerhub-token: + description: "Docker Hub token/password (pass from secrets)" + required: true + +runs: + using: "composite" + steps: + - uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 + - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + username: ${{ inputs.dockerhub-username }} + password: ${{ inputs.dockerhub-token }} diff --git a/.github/actions/setup-hatch/action.yml b/.github/actions/setup-hatch/action.yml new file mode 100644 index 0000000..0da5160 --- /dev/null +++ b/.github/actions/setup-hatch/action.yml @@ -0,0 +1,13 @@ +name: "Set up Hatch build tooling" +description: >- + Install the pinned hatch / hatchling / virtualenv toolchain used to build + and publish the package. Assumes Python is already set up by the caller. + +runs: + using: "composite" + steps: + - shell: bash + run: | + python -m pip install --upgrade pip + pip install "virtualenv<20.36" + pip install hatchling==1.27.0 hatch==1.14.0 diff --git a/.github/actions/setup-sfw/action.yml b/.github/actions/setup-sfw/action.yml new file mode 100644 index 0000000..456f90b --- /dev/null +++ b/.github/actions/setup-sfw/action.yml @@ -0,0 +1,49 @@ +name: "Set up Socket Firewall" +description: >- + Set up the requested language toolchain and install Socket Firewall (free + or enterprise edition) so subsequent steps can run package-manager commands + wrapped with `sfw`. Defaults to free/anonymous mode (no API token -- safe on + untrusted / Dependabot / fork PRs). Pass mode: firewall-enterprise + + socket-token for full org-policy enforcement on trusted maintainer PRs. + +inputs: + python: + description: "Set up Python 3.12" + default: "false" + node: + description: "Set up Node 20 (needed for npm-wrapped checks)" + default: "false" + uv: + description: "Install uv (implies Python)" + default: "false" + mode: + description: "socketdev/action mode: firewall-free or firewall-enterprise" + default: "firewall-free" + socket-token: + description: "Socket API token (only used/required for firewall-enterprise)" + default: "" + +runs: + using: "composite" + steps: + - if: ${{ inputs.python == 'true' || inputs.uv == 'true' }} + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.12" + + - if: ${{ inputs.node == 'true' }} + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: "20" + + # Official Socket setup action. Wires up sfw routing correctly. + # socket-token is ignored in firewall-free mode and empty when absent. + - uses: socketdev/action@ba6de6cc0565af1f42295590380973573297e31f # v1.3.2 + with: + mode: ${{ inputs.mode }} + socket-token: ${{ inputs.socket-token }} + + - if: ${{ inputs.uv == 'true' }} + name: Install uv + shell: bash + run: python -m pip install --upgrade pip uv diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7c05ee5..89e2ed0 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -36,9 +36,11 @@ updates: cooldown: default-days: 7 - # GitHub Actions used in workflows + # GitHub Actions used in workflows and local composite actions. - package-ecosystem: "github-actions" - directory: "/" + directories: + - "/" + - "/.github/actions/*" schedule: interval: "weekly" open-pull-requests-limit: 2 diff --git a/.github/workflows/dependabot-review.yml b/.github/workflows/dependabot-review.yml deleted file mode 100644 index 486ccb3..0000000 --- a/.github/workflows/dependabot-review.yml +++ /dev/null @@ -1,205 +0,0 @@ -name: dependabot-review - -# Dependency-update PR guardrails for Dependabot-authored PRs. -# -# Runs only on PRs opened by dependabot[bot]. Inspects which files -# changed, then conditionally runs Socket Firewall (sfw) install smoke -# jobs for the affected manifests. Because sfw uses the free, anonymous -# Socket public-data path it needs NO API key, so we can run it from -# the unprivileged `pull_request` context without pull_request_target -# or any of its security tradeoffs. -# -# Pattern adapted from SocketDev/socket-basics. - -on: - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - -permissions: - contents: read - -concurrency: - group: dependabot-review-${{ github.event.pull_request.number }} - cancel-in-progress: true - -jobs: - inspect: - if: github.event.pull_request.user.login == 'dependabot[bot]' - runs-on: ubuntu-latest - timeout-minutes: 5 - outputs: - python_deps_changed: ${{ steps.diff.outputs.python_deps_changed }} - fixture_npm_changed: ${{ steps.diff.outputs.fixture_npm_changed }} - fixture_pypi_changed: ${{ steps.diff.outputs.fixture_pypi_changed }} - dockerfile_changed: ${{ steps.diff.outputs.dockerfile_changed }} - workflow_or_action_changed: ${{ steps.diff.outputs.workflow_or_action_changed }} - steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Inspect changed files - id: diff - env: - BASE_SHA: ${{ github.event.pull_request.base.sha }} - HEAD_SHA: ${{ github.event.pull_request.head.sha }} - run: | - CHANGED_FILES="$(git diff --name-only "$BASE_SHA" "$HEAD_SHA")" - - { - echo "## Changed files" - echo '```' - printf '%s\n' "$CHANGED_FILES" - echo '```' - } >> "$GITHUB_STEP_SUMMARY" - - has_file() { - local pattern="$1" - if printf '%s\n' "$CHANGED_FILES" | grep -Eq "$pattern"; then - echo "true" - else - echo "false" - fi - } - - { - echo "python_deps_changed=$(has_file '^(pyproject\.toml|uv\.lock)$')" - echo "fixture_npm_changed=$(has_file '^tests/e2e/fixtures/simple-npm/')" - echo "fixture_pypi_changed=$(has_file '^tests/e2e/fixtures/simple-pypi/')" - echo "dockerfile_changed=$(has_file '^Dockerfile$')" - echo "workflow_or_action_changed=$(has_file '^\.github/workflows/|^\.github/dependabot\.yml$')" - } >> "$GITHUB_OUTPUT" - - - name: Summarize review expectations - env: - PR_URL: ${{ github.event.pull_request.html_url }} - run: | - { - echo "## Dependabot Review Checklist" - echo "- PR: $PR_URL" - echo "- Confirm upstream release notes before merge" - echo "- Do not treat a Dependabot PR as trusted solely because of the actor" - echo "- This workflow runs in pull_request context only; no publish secrets are exposed" - } >> "$GITHUB_STEP_SUMMARY" - - python-sfw-smoke: - needs: inspect - if: needs.inspect.outputs.python_deps_changed == 'true' - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 - with: - fetch-depth: 1 - persist-credentials: false - - - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 - with: - python-version: "3.12" - - - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af - with: - node-version: "20" - - - name: Install Socket Firewall - run: npm install -g sfw - - - name: Install uv - run: python -m pip install --upgrade pip uv - - - name: Sync project through Socket Firewall - run: sfw uv sync --extra test --extra dev - - - name: Import smoke test - run: | - uv run python -c " - from socketsecurity.socketcli import cli, build_socket_sdk - from socketsecurity.core import Core - from socketsecurity.core.exceptions import ( - APIFailure, RequestTimeoutExceeded, APIResourceNotFound, - ) - from socketsecurity.core.git_interface import Git - from socketsecurity.config import CliConfig - print('import smoke OK') - " - - fixture-npm-sfw-smoke: - needs: inspect - if: needs.inspect.outputs.fixture_npm_changed == 'true' - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 - with: - fetch-depth: 1 - persist-credentials: false - - - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af - with: - node-version: "20" - - - name: Install Socket Firewall - run: npm install -g sfw - - - name: Install fixture through Socket Firewall - working-directory: tests/e2e/fixtures/simple-npm - run: sfw npm install --no-audit --no-fund --ignore-scripts - - fixture-pypi-sfw-smoke: - needs: inspect - if: needs.inspect.outputs.fixture_pypi_changed == 'true' - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 - with: - fetch-depth: 1 - persist-credentials: false - - - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 - with: - python-version: "3.12" - - - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af - with: - node-version: "20" - - - name: Install Socket Firewall - run: npm install -g sfw - - - name: Install fixture through Socket Firewall - working-directory: tests/e2e/fixtures/simple-pypi - run: | - python -m venv .venv - # shellcheck disable=SC1091 - source .venv/bin/activate - sfw pip install -r requirements.txt - - dockerfile-smoke: - needs: inspect - if: needs.inspect.outputs.dockerfile_changed == 'true' - runs-on: ubuntu-latest - timeout-minutes: 20 - steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 - with: - fetch-depth: 1 - persist-credentials: false - - - name: Build the Dockerfile (no push) - run: docker build --pull -t socket-python-cli:dependabot-smoke . - - workflow-notice: - needs: inspect - if: needs.inspect.outputs.workflow_or_action_changed == 'true' - runs-on: ubuntu-latest - timeout-minutes: 2 - steps: - - name: Flag workflow-sensitive updates - run: | - { - echo "## Sensitive File Notice" - echo "This Dependabot PR changes workflow or dependabot config files." - echo "Require explicit human review before merge." - } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..dffa8a8 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,353 @@ +name: dependency-review + +# Supply-chain guardrails for dependency-update PRs -- for BOTH Dependabot +# and maintainers. +# +# Inspects the changed files, then conditionally runs Socket Firewall (sfw) +# install smoke jobs for the affected manifests, picking the firewall edition +# per PR: +# +# - Trusted authors: any in-repo (non-fork) PR other than Dependabot's +# (i.e. someone with write access) -> Socket Firewall ENTERPRISE through +# the socket-firewall environment and its SOCKET_SFW_API_TOKEN secret +# (authenticated, full org-policy enforcement). +# - Everyone else: Dependabot and all fork PRs from external contributors -> +# Socket Firewall FREE (anonymous, no API token), which is safe in the +# unprivileged `pull_request` context. +# +# Only Enterprise jobs declare the socket-firewall environment. Free jobs do +# not touch that environment or its token. +# +# Pattern adapted from SocketDev/socket-basics. + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +permissions: + contents: read + +concurrency: + group: dependency-review-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + inspect: + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + python_deps_changed: ${{ steps.diff.outputs.python_deps_changed }} + fixture_npm_changed: ${{ steps.diff.outputs.fixture_npm_changed }} + fixture_pypi_changed: ${{ steps.diff.outputs.fixture_pypi_changed }} + dockerfile_changed: ${{ steps.diff.outputs.dockerfile_changed }} + workflow_or_action_changed: ${{ steps.diff.outputs.workflow_or_action_changed }} + sfw_mode: ${{ steps.mode.outputs.sfw_mode }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Inspect changed files + id: diff + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + CHANGED_FILES="$(git diff --name-only "$BASE_SHA" "$HEAD_SHA")" + + { + echo "## Changed files" + echo '```' + printf '%s\n' "$CHANGED_FILES" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + + has_file() { + local pattern="$1" + if printf '%s\n' "$CHANGED_FILES" | grep -Eq "$pattern"; then + echo "true" + else + echo "false" + fi + } + + { + echo "python_deps_changed=$(has_file '^(pyproject\.toml|uv\.lock)$')" + echo "fixture_npm_changed=$(has_file '^tests/e2e/fixtures/simple-npm/')" + echo "fixture_pypi_changed=$(has_file '^tests/e2e/fixtures/simple-pypi/')" + echo "dockerfile_changed=$(has_file '^Dockerfile$')" + echo "workflow_or_action_changed=$(has_file '^\.github/workflows/|^\.github/actions/|^\.github/dependabot\.yml$')" + } >> "$GITHUB_OUTPUT" + + - name: Determine Socket Firewall mode + id: mode + # Trusted == any in-repo (non-fork) PR that isn't Dependabot's. Only + # accounts with write access can push a branch to this repo, so a + # non-fork PR already implies a trusted author -- the same boundary + # GitHub uses to decide whether secrets are exposed at all. + # + # NB: author_association is deliberately NOT used to require strict org + # membership. It only reflects PUBLIC org membership, so private members + # (the common case) show up as CONTRIBUTOR and would be misclassified. + # Reliable strict-membership detection would need a read:org token or + # public membership. This step references NO secret regardless -- it + # only decides which smoke job runs. + env: + IS_DEPENDABOT: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }} + IS_FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }} + AUTHOR_ASSOC: ${{ github.event.pull_request.author_association }} + run: | + mode=firewall-free + if [ "$IS_DEPENDABOT" != "true" ] && [ "$IS_FORK" != "true" ]; then + mode=firewall-enterprise + fi + + echo "sfw_mode=$mode" >> "$GITHUB_OUTPUT" + { + echo "## Socket Firewall mode: \`$mode\`" + echo "- author_association: \`$AUTHOR_ASSOC\`" + echo "- dependabot: \`$IS_DEPENDABOT\` | fork: \`$IS_FORK\`" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Summarize review expectations + env: + PR_URL: ${{ github.event.pull_request.html_url }} + run: | + { + echo "## Dependency Review Checklist" + echo "- PR: $PR_URL" + echo "- Confirm upstream release notes before merge" + echo "- Do not treat a dependency PR as trusted solely because of the actor" + echo "- This workflow runs in pull_request context only; no publish secrets are exposed" + } >> "$GITHUB_STEP_SUMMARY" + + python-sfw-smoke-free: + needs: inspect + if: | + needs.inspect.outputs.python_deps_changed == 'true' && + needs.inspect.outputs.sfw_mode == 'firewall-free' + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + persist-credentials: false + + - uses: ./.github/actions/setup-sfw + with: + uv: "true" + mode: firewall-free + + - name: Sync project through Socket Firewall + # `sfw uv sync` is the intended way to route uv through Socket Firewall + # (per Socket's own uv wrapper guidance). --locked verifies the exact + # uv.lock set and fails on lockfile drift rather than silently + # re-resolving, so the firewall inspects precisely what would install. + # Note: uv's sfw integration is quieter than npm/pip -- it does not + # print the "N packages fetched" footer, but interception is active. + # + # Use the runner's setup-python interpreter and forbid managed-Python + # downloads. The firewall is here to vet PyPI installs, not the + # interpreter/toolchain download path. + env: + UV_PYTHON: "3.12" + UV_PYTHON_DOWNLOADS: never + run: sfw uv sync --locked --extra test --extra dev + + - name: Import smoke test + env: + UV_PYTHON: "3.12" + UV_PYTHON_DOWNLOADS: never + run: | + uv run python -c " + from socketsecurity.socketcli import cli, build_socket_sdk + from socketsecurity.core import Core + from socketsecurity.core.exceptions import ( + APIFailure, RequestTimeoutExceeded, APIResourceNotFound, + ) + from socketsecurity.core.git_interface import Git + from socketsecurity.config import CliConfig + print('import smoke OK') + " + + python-sfw-smoke-enterprise: + needs: inspect + if: | + needs.inspect.outputs.python_deps_changed == 'true' && + needs.inspect.outputs.sfw_mode == 'firewall-enterprise' + runs-on: ubuntu-latest + timeout-minutes: 15 + environment: socket-firewall + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + persist-credentials: false + + - uses: ./.github/actions/setup-sfw + with: + uv: "true" + mode: firewall-enterprise + socket-token: ${{ secrets.SOCKET_SFW_API_TOKEN }} + + - name: Sync project through Socket Firewall + # `sfw uv sync` is the intended way to route uv through Socket Firewall + # (per Socket's own uv wrapper guidance). --locked verifies the exact + # uv.lock set and fails on lockfile drift rather than silently + # re-resolving, so the firewall inspects precisely what would install. + # Note: uv's sfw integration is quieter than npm/pip -- it does not + # print the "N packages fetched" footer, but interception is active. + # + # Use the runner's setup-python interpreter and forbid managed-Python + # downloads. The firewall is here to vet PyPI installs, not the + # interpreter/toolchain download path. + env: + UV_PYTHON: "3.12" + UV_PYTHON_DOWNLOADS: never + run: sfw uv sync --locked --extra test --extra dev + + - name: Import smoke test + env: + UV_PYTHON: "3.12" + UV_PYTHON_DOWNLOADS: never + run: | + uv run python -c " + from socketsecurity.socketcli import cli, build_socket_sdk + from socketsecurity.core import Core + from socketsecurity.core.exceptions import ( + APIFailure, RequestTimeoutExceeded, APIResourceNotFound, + ) + from socketsecurity.core.git_interface import Git + from socketsecurity.config import CliConfig + print('import smoke OK') + " + + fixture-npm-sfw-smoke-free: + needs: inspect + if: | + needs.inspect.outputs.fixture_npm_changed == 'true' && + needs.inspect.outputs.sfw_mode == 'firewall-free' + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + persist-credentials: false + + - uses: ./.github/actions/setup-sfw + with: + node: "true" + mode: firewall-free + + - name: Install fixture through Socket Firewall + working-directory: tests/e2e/fixtures/simple-npm + run: sfw npm install --no-audit --no-fund --ignore-scripts + + fixture-npm-sfw-smoke-enterprise: + needs: inspect + if: | + needs.inspect.outputs.fixture_npm_changed == 'true' && + needs.inspect.outputs.sfw_mode == 'firewall-enterprise' + runs-on: ubuntu-latest + timeout-minutes: 15 + environment: socket-firewall + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + persist-credentials: false + + - uses: ./.github/actions/setup-sfw + with: + node: "true" + mode: firewall-enterprise + socket-token: ${{ secrets.SOCKET_SFW_API_TOKEN }} + + - name: Install fixture through Socket Firewall + working-directory: tests/e2e/fixtures/simple-npm + run: sfw npm install --no-audit --no-fund --ignore-scripts + + fixture-pypi-sfw-smoke-free: + needs: inspect + if: | + needs.inspect.outputs.fixture_pypi_changed == 'true' && + needs.inspect.outputs.sfw_mode == 'firewall-free' + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + persist-credentials: false + + - uses: ./.github/actions/setup-sfw + with: + python: "true" + mode: firewall-free + + - name: Install fixture through Socket Firewall + working-directory: tests/e2e/fixtures/simple-pypi + run: | + python -m venv .venv + # shellcheck disable=SC1091 + source .venv/bin/activate + sfw pip install -r requirements.txt + + fixture-pypi-sfw-smoke-enterprise: + needs: inspect + if: | + needs.inspect.outputs.fixture_pypi_changed == 'true' && + needs.inspect.outputs.sfw_mode == 'firewall-enterprise' + runs-on: ubuntu-latest + timeout-minutes: 15 + environment: socket-firewall + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + persist-credentials: false + + - uses: ./.github/actions/setup-sfw + with: + python: "true" + mode: firewall-enterprise + socket-token: ${{ secrets.SOCKET_SFW_API_TOKEN }} + + - name: Install fixture through Socket Firewall + working-directory: tests/e2e/fixtures/simple-pypi + run: | + python -m venv .venv + # shellcheck disable=SC1091 + source .venv/bin/activate + sfw pip install -r requirements.txt + + dockerfile-smoke: + needs: inspect + if: needs.inspect.outputs.dockerfile_changed == 'true' + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Build the Dockerfile (no push) + run: docker build --pull -t socket-python-cli:dependabot-smoke . + + workflow-notice: + needs: inspect + if: needs.inspect.outputs.workflow_or_action_changed == 'true' + runs-on: ubuntu-latest + timeout-minutes: 2 + steps: + - name: Flag workflow-sensitive updates + run: | + { + echo "## Sensitive File Notice" + echo "This PR changes workflow, composite-action, or dependabot config files." + echo "Require explicit human review before merge." + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/docker-stable.yml b/.github/workflows/docker-stable.yml index 3639ffc..934e0d9 100644 --- a/.github/workflows/docker-stable.yml +++ b/.github/workflows/docker-stable.yml @@ -13,7 +13,7 @@ jobs: stable: runs-on: ubuntu-latest steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -28,23 +28,19 @@ jobs: fi echo "Version ${INPUT_VERSION} found on PyPI - proceeding with release" - - name: Set up QEMU - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - - - name: Login to Docker Hub with Organization Token - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + - name: Set up Docker publishing + uses: ./.github/actions/setup-docker with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build & Push Stable Docker - uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: push: true platforms: linux/amd64,linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max tags: socketdev/cli:stable build-args: | CLI_VERSION=${{ inputs.version }} diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 83a6fa4..f233115 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -14,7 +14,7 @@ jobs: # Skip e2e on: # - PRs from forks (no secrets) # - Dependabot PRs (no secrets, and dependency-bump risk is already - # covered by dependabot-review.yml's Socket Firewall smoke jobs) + # covered by dependency-review.yml's Socket Firewall smoke jobs) if: >- (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && @@ -70,16 +70,16 @@ jobs: name: e2e-${{ matrix.name }} steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.12' - - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 if: matrix.setup-node == 'true' with: node-version: '20' diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml index 1d7115a..eb29ef9 100644 --- a/.github/workflows/pr-preview.yml +++ b/.github/workflows/pr-preview.yml @@ -3,29 +3,37 @@ on: pull_request: types: [opened, synchronize, ready_for_review] +# Cancel an in-flight preview when the PR is pushed again -- previews are slow +# (publish + multi-step Docker build), so superseded runs shouldn't keep going. +concurrency: + group: pr-preview-${{ github.event.pull_request.number }} + cancel-in-progress: true + jobs: preview: - if: github.event.pull_request.head.repo.full_name == github.repository + # Skip on: + # - PRs from forks (no access to publish secrets) + # - Dependabot PRs: preview-publishing a dependency bump to Test PyPI / + # Docker Hub is pointless and fails (no version bump, secret access). + if: >- + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.user.login != 'dependabot[bot]' runs-on: ubuntu-latest permissions: id-token: write contents: read pull-requests: write steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.13' - # Install all dependencies from pyproject.toml - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install "virtualenv<20.36" - pip install hatchling==1.27.0 hatch==1.14.0 + - name: Install build tooling + uses: ./.github/actions/setup-hatch - name: Inject full dynamic version run: python .hooks/sync_version.py --dev @@ -57,14 +65,14 @@ jobs: - name: Publish to Test PyPI if: steps.version_check.outputs.exists != 'true' - uses: pypa/gh-action-pypi-publish@ab69e431e9c9f48a3310be0a56527c679f56e04d + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: repository-url: https://test.pypi.org/legacy/ verbose: true - name: Comment on PR if: steps.version_check.outputs.exists != 'true' - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: VERSION: ${{ env.VERSION }} with: @@ -133,27 +141,26 @@ jobs: echo "success=false" >> $GITHUB_OUTPUT exit 1 - - name: Set up QEMU - uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 - - - name: Login to Docker Hub with Organization Token + - name: Set up Docker publishing if: steps.verify_package.outputs.success == 'true' - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 + uses: ./.github/actions/setup-docker with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build & Push Docker Preview if: steps.verify_package.outputs.success == 'true' - uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 env: VERSION: ${{ env.VERSION }} with: push: true - platforms: linux/amd64,linux/arm64 + # Preview images are for quick testing -- build amd64 only. arm64 via + # QEMU emulation is the slowest part of the job; release builds keep + # multi-arch. GHA layer cache speeds up repeated preview builds. + platforms: linux/amd64 + cache-from: type=gha + cache-to: type=gha,mode=max tags: | socketdev/cli:pr-${{ github.event.pull_request.number }} build-args: | diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 94f4f82..3247275 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -35,12 +35,12 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 persist-credentials: false - name: 🐍 setup python - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ env.PYTHON_VERSION }} - name: 🛠️ install deps @@ -71,12 +71,12 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 1 persist-credentials: false - name: 🐍 setup python - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.10" - name: 🚫 verify install is rejected on unsupported python diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5549b88..6c41e9c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,21 +10,17 @@ jobs: id-token: write contents: read steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.13' - # Install all dependencies from pyproject.toml - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install "virtualenv<20.36" - pip install hatchling==1.27.0 hatch==1.14.0 - + - name: Install build tooling + uses: ./.github/actions/setup-hatch + - name: Get Version id: version env: @@ -70,19 +66,13 @@ jobs: - name: Publish to PyPI if: steps.version_check.outputs.pypi_exists != 'true' - uses: pypa/gh-action-pypi-publish@ab69e431e9c9f48a3310be0a56527c679f56e04d - - - name: Set up QEMU - uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 - - name: Login to Docker Hub with Organization Token - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 + - name: Set up Docker publishing + uses: ./.github/actions/setup-docker with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} - name: Verify package is installable id: verify_package @@ -106,12 +96,14 @@ jobs: if: | steps.verify_package.outputs.success == 'true' && steps.docker_check.outputs.docker_exists != 'true' - uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 env: VERSION: ${{ env.VERSION }} with: push: true platforms: linux/amd64,linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max tags: | socketdev/cli:latest socketdev/cli:${{ env.VERSION }} diff --git a/.github/workflows/version-check.yml b/.github/workflows/version-check.yml index 1eefa27..a097208 100644 --- a/.github/workflows/version-check.yml +++ b/.github/workflows/version-check.yml @@ -14,9 +14,13 @@ permissions: jobs: check_version: + # Skip on Dependabot PRs: they bump dependencies (touching uv.lock / + # pyproject.toml) without bumping the app version, so the increment check + # would always fail. App-version bumps come from maintainer PRs. + if: github.event.pull_request.user.login != 'dependabot[bot]' runs-on: ubuntu-latest steps: - - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 # Fetch all history for all branches persist-credentials: false @@ -86,7 +90,7 @@ jobs: fi - name: Manage PR Comment - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 if: always() && github.event.pull_request.head.repo.full_name == github.repository env: MAIN_VERSION: ${{ env.MAIN_VERSION }}