From 47c6aad2ecf7c162b527e3e6223aaba0541c2f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Garillot?= Date: Fri, 10 Apr 2026 09:43:04 -0400 Subject: [PATCH] ci: harden reusable GitHub Actions --- .github/actions/debian/action.yml | 116 ++++++++++++--------- .github/workflows/book.yml | 24 +++-- .github/workflows/build-docker.yml | 27 ++--- .github/workflows/changelog.yml | 8 +- .github/workflows/contribution-quality.yml | 36 +++++-- .github/workflows/lint.yml | 59 ++++++++--- .github/workflows/publish-debian.yml | 32 +++++- .github/workflows/publish-docker.yml | 17 ++- .github/workflows/release-plz-dry-run.yml | 9 +- .github/workflows/release-plz.yml | 15 ++- .github/workflows/signed-commits.yml | 12 ++- .github/workflows/test.yml | 21 +++- .github/workflows/zizmor.yml | 40 +++++++ 13 files changed, 304 insertions(+), 112 deletions(-) create mode 100644 .github/workflows/zizmor.yml diff --git a/.github/actions/debian/action.yml b/.github/actions/debian/action.yml index 9f85981..4c74426 100644 --- a/.github/actions/debian/action.yml +++ b/.github/actions/debian/action.yml @@ -10,36 +10,27 @@ inputs: target_branch: description: The target branch required: false - type: string default: 'next' arch: required: true description: Machine architecture to build packages for. - type: choice - options: - - amd64 - - arm64 crate: required: true description: Name of binary crate being packaged. - type: choice crate_dir: required: true description: Name of crate being packaged. - type: string service: required: true description: The service to build the packages for. - type: string package: required: true description: Name of packaging directory. - type: string runs: using: "composite" steps: - name: Rust cache - uses: Swatinem/rust-cache@v2 + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: # Only update the cache on push onto the target branch. This strikes a nice balance between # cache hits and cache evictions (github has a 10GB cache limit). @@ -53,13 +44,16 @@ runs: - name: Identify target git SHA id: git-sha shell: bash + env: + # Keep the workflow expression out of the shell body; quoting $GITREF below prevents command injection. + GITREF: ${{ inputs.gitref }} run: | - if git show-ref -q --verify "refs/remotes/origin/${{ inputs.gitref }}" 2>/dev/null; then - echo "sha=$(git show-ref --hash --verify 'refs/remotes/origin/${{ inputs.gitref }}')" >> $GITHUB_OUTPUT - elif git show-ref -q --verify "refs/tags/${{ inputs.gitref }}" 2>/dev/null; then - echo "sha=$(git show-ref --hash --verify 'refs/tags/${{ inputs.gitref }}')" >> $GITHUB_OUTPUT - elif git rev-parse --verify "${{ inputs.gitref }}^{commit}" >/dev/null 2>&1; then - echo "sha=$(git rev-parse --verify '${{ inputs.gitref }}^{commit}')" >> $GITHUB_OUTPUT + if git show-ref -q --verify "refs/remotes/origin/${GITREF}" 2>/dev/null; then + printf 'sha=%s\n' "$(git show-ref --hash --verify "refs/remotes/origin/${GITREF}")" >> "$GITHUB_OUTPUT" + elif git show-ref -q --verify "refs/tags/${GITREF}" 2>/dev/null; then + printf 'sha=%s\n' "$(git show-ref --hash --verify "refs/tags/${GITREF}")" >> "$GITHUB_OUTPUT" + elif git rev-parse --verify "${GITREF}^{commit}" >/dev/null 2>&1; then + printf 'sha=%s\n' "$(git rev-parse --verify "${GITREF}^{commit}")" >> "$GITHUB_OUTPUT" else echo "::error::Unknown git reference type" exit 1 @@ -67,30 +61,38 @@ runs: - name: Create package directories shell: bash + env: + SERVICE: ${{ inputs.service }} run: | - pkg=${{ inputs.service }} + pkg="$SERVICE" mkdir -p \ - packaging/deb/$pkg/DEBIAN \ - packaging/deb/$pkg/usr/bin \ - packaging/deb/$pkg/lib/systemd/system \ - packaging/deb/$pkg/opt/$pkg \ - done + "packaging/deb/$pkg/DEBIAN" \ + "packaging/deb/$pkg/usr/bin" \ + "packaging/deb/$pkg/lib/systemd/system" \ + "packaging/deb/$pkg/opt/$pkg" - name: Copy package install scripts shell: bash + env: + TARGET_SHA: ${{ steps.git-sha.outputs.sha }} + SERVICE: ${{ inputs.service }} + PACKAGE_DIR: ${{ inputs.package }} + CRATE_DIR: ${{ inputs.crate_dir }} run: | - svc=${{ inputs.service }} - pkg=${{ inputs.package }} - crate=${{ inputs.crate_dir }} - git show ${{ steps.git-sha.outputs.sha }}:bin/$crate/.env > packaging/deb/$svc/lib/systemd/system/$svc.env - git show ${{ steps.git-sha.outputs.sha }}:packaging/$pkg/$svc.service > packaging/deb/$svc/lib/systemd/system/$svc.service - git show ${{ steps.git-sha.outputs.sha }}:packaging/$pkg/postinst > packaging/deb/$svc/DEBIAN/postinst - git show ${{ steps.git-sha.outputs.sha }}:packaging/$pkg/postrm > packaging/deb/$svc/DEBIAN/postrm - chmod 0775 packaging/deb/$svc/DEBIAN/postinst - chmod 0775 packaging/deb/$svc/DEBIAN/postrm + svc="$SERVICE" + pkg="$PACKAGE_DIR" + crate="$CRATE_DIR" + git show "${TARGET_SHA}:bin/$crate/.env" > "packaging/deb/$svc/lib/systemd/system/$svc.env" + git show "${TARGET_SHA}:packaging/$pkg/$svc.service" > "packaging/deb/$svc/lib/systemd/system/$svc.service" + git show "${TARGET_SHA}:packaging/$pkg/postinst" > "packaging/deb/$svc/DEBIAN/postinst" + git show "${TARGET_SHA}:packaging/$pkg/postrm" > "packaging/deb/$svc/DEBIAN/postrm" + chmod 0775 "packaging/deb/$svc/DEBIAN/postinst" + chmod 0775 "packaging/deb/$svc/DEBIAN/postrm" - name: Create control files shell: bash + env: + SERVICE: ${{ inputs.service }} run: | # Map the architecture to the format required by Debian. # i.e. arm64 and amd64 instead of aarch64 and x86_64. @@ -98,8 +100,8 @@ runs: # Control file's version field must be x.y.z format so strip the rest. version=$(git describe --tags --abbrev=0 | sed 's/[^0-9.]//g' ) - pkg=${{ inputs.service }} - cat > packaging/deb/$pkg/DEBIAN/control << EOF + pkg="$SERVICE" + cat > "packaging/deb/$pkg/DEBIAN/control" << EOF Package: $pkg Version: $version Section: base @@ -115,47 +117,63 @@ runs: - name: Build binaries shell: bash env: - repo-url: ${{ github.server_url }}/${{ github.repository }} + CRATE: ${{ inputs.crate }} + REPO_URL: ${{ github.server_url }}/${{ github.repository }} + TARGET_SHA: ${{ steps.git-sha.outputs.sha }} run: | - cargo install ${{ inputs.crate }} --root . --locked --git ${{ env.repo-url }} --rev ${{ steps.git-sha.outputs.sha }} + cargo install "$CRATE" --root . --locked --git "$REPO_URL" --rev "$TARGET_SHA" - name: Copy binary files shell: bash + env: + SERVICE: ${{ inputs.service }} + CRATE: ${{ inputs.crate }} run: | - pkg=${{ inputs.service }} - bin=${{ inputs.crate }} - cp -p ./bin/$bin packaging/deb/$pkg/usr/bin/ + pkg="$SERVICE" + bin="$CRATE" + cp -p "./bin/$bin" "packaging/deb/$pkg/usr/bin/" - name: Build packages shell: bash + env: + SERVICE: ${{ inputs.service }} run: | - dpkg-deb --build --root-owner-group packaging/deb/${{ inputs.service }} + dpkg-deb --build --root-owner-group "packaging/deb/$SERVICE" # Save the .deb files, delete the rest. mv packaging/deb/*.deb . rm -rf packaging - - name: Package names - shell: bash - run: | - echo "package=${{ inputs.service }}-${{ inputs.gitref }}-${{ inputs.arch }}.deb" >> $GITHUB_ENV - - name: Rename package files shell: bash + env: + SERVICE: ${{ inputs.service }} + GITREF: ${{ inputs.gitref }} + ARCH: ${{ inputs.arch }} run: | - mv ${{ inputs.service }}.deb ${{ env.package }} + package="${SERVICE}-${GITREF}-${ARCH}.deb" + mv "${SERVICE}.deb" "$package" - name: shasum packages shell: bash + env: + SERVICE: ${{ inputs.service }} + GITREF: ${{ inputs.gitref }} + ARCH: ${{ inputs.arch }} run: | - sha256sum ${{ env.package }} > ${{ env.package }}.checksum + package="${SERVICE}-${GITREF}-${ARCH}.deb" + sha256sum "$package" > "${package}.checksum" - name: Publish packages shell: bash env: GH_TOKEN: ${{ inputs.github_token }} + SERVICE: ${{ inputs.service }} + GITREF: ${{ inputs.gitref }} + ARCH: ${{ inputs.arch }} run: | - gh release upload ${{ inputs.gitref }} \ - ${{ env.package }} \ - ${{ env.package }}.checksum \ + package="${SERVICE}-${GITREF}-${ARCH}.deb" + gh release upload "$GITREF" \ + "$package" \ + "${package}.checksum" \ --clobber diff --git a/.github/workflows/book.yml b/.github/workflows/book.yml index 3fd3f9c..b7f96bd 100644 --- a/.github/workflows/book.yml +++ b/.github/workflows/book.yml @@ -28,6 +28,9 @@ on: type: string default: 'next' +# Default the token to no scopes; each job opts into only the scopes it needs. +permissions: {} + jobs: # Always build and test the mdbook documentation whenever the docs folder is changed. # @@ -35,27 +38,33 @@ jobs: build: name: Build documentation runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false # Installation from source takes a fair while, so we install the binaries directly instead. - name: Install mdbook and plugins - uses: taiki-e/install-action@v2 + uses: taiki-e/install-action@97a5807a604e12de3a13b52d868ebecaeeea757c # v2 with: tool: mdbook, mdbook-linkcheck, mdbook-alerts, mdbook-katex - name: Build book - run: mdbook build ${{ inputs.directory }} + env: + MDBOOK_DIRECTORY: ${{ inputs.directory }} + run: mdbook build "$MDBOOK_DIRECTORY" # Only Upload documentation if we want to deploy (i.e. push to the branch). - name: Setup Pages if: ${{ github.event_name == 'push' && github.ref == format('refs/heads/{0}', inputs.target_branch) }} id: pages - uses: actions/configure-pages@v5 + uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5 - name: Upload book artifact if: ${{ github.event_name == 'push' && github.ref == format('refs/heads/{0}', inputs.target_branch) }} - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3 with: # We specify multiple [output] sections in our book.toml which causes mdbook to create separate folders for each. This moves the generated `html` into its own `html` subdirectory. path: ${{ inputs.directory }}/${{ inputs.artifact_path }} @@ -69,7 +78,10 @@ jobs: runs-on: ubuntu-latest needs: build if: ${{ github.event_name == 'push' && github.ref == format('refs/heads/{0}', inputs.target_branch) }} + permissions: + id-token: write + pages: write steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index a8d7a1d..4654288 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -37,37 +37,40 @@ on: description: 'AWS cache bucket' required: false +# Default the token to no scopes; each job opts into only the scopes it needs. +permissions: {} + jobs: docker-build: runs-on: ubuntu-latest name: Build ${{ inputs.component }} + permissions: + contents: read + id-token: write steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - name: Configure AWS credentials if: ${{ github.event.pull_request.head.repo.fork == false && inputs.use_cache }} - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4 with: aws-region: ${{ secrets.aws_region }} role-to-assume: ${{ secrets.aws_role }} role-session-name: GithubActionsSession - - name: Set cache parameters - if: ${{ github.event.pull_request.head.repo.fork == false && inputs.use_cache }} - run: | - echo "CACHE_FROM=type=s3,region=${{ secrets.aws_region }},bucket=${{ secrets.aws_cache_bucket }},name=miden-${{ inputs.component }}" >> $GITHUB_ENV - echo "CACHE_TO=type=s3,region=${{ secrets.aws_region }},bucket=${{ secrets.aws_cache_bucket }},name=miden-${{ inputs.component }}" >> $GITHUB_ENV - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 with: cache-binary: true - name: Build Docker image - uses: docker/build-push-action@v5 + uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5 with: push: false file: ${{ inputs.dockerfile_path }} - cache-from: ${{ env.CACHE_FROM || '' }} - cache-to: ${{ env.CACHE_TO || '' }} + # BuildKit cache settings stay in expressions rather than a shell step, avoiding command-injection risk. + cache-from: ${{ github.event.pull_request.head.repo.fork == false && inputs.use_cache && format('type=s3,region={0},bucket={1},name=miden-{2}', secrets.aws_region, secrets.aws_cache_bucket, inputs.component) || '' }} + cache-to: ${{ github.event.pull_request.head.repo.fork == false && inputs.use_cache && format('type=s3,region={0},bucket={1},name=miden-{2}', secrets.aws_region, secrets.aws_cache_bucket, inputs.component) || '' }} diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index bbf0d0b..21b58fb 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -12,14 +12,20 @@ on: type: string default: 'CHANGELOG.md' +# Default the token to no scopes; this check only needs repository read access. +permissions: {} + jobs: changelog: runs-on: ubuntu-latest + permissions: + contents: read steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 + persist-credentials: false - name: Check for changes in changelog env: BASE_REF: ${{ github.event.pull_request.base.ref }} diff --git a/.github/workflows/contribution-quality.yml b/.github/workflows/contribution-quality.yml index c1d1f4f..fcebe78 100644 --- a/.github/workflows/contribution-quality.yml +++ b/.github/workflows/contribution-quality.yml @@ -22,16 +22,23 @@ on: type: string default: "false" +# Default the token to no scopes; this metadata-only workflow opts into PR and issue API scopes. +permissions: {} + jobs: gate: runs-on: ubuntu-latest + permissions: + contents: read + issues: write + pull-requests: read env: DISPATCH_PR_NUMBER: ${{ inputs.pr_number }} DISPATCH_FORCE_ALL: ${{ inputs.force_all }} steps: - name: Resolve PR number and event mode id: ctx - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: script: | const isPREvent = !!context.payload.pull_request; @@ -47,10 +54,12 @@ jobs: - name: Load PR details id: pr - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + env: + PR_NUMBER: ${{ steps.ctx.outputs.pr_number }} with: script: | - const prNumber = parseInt("${{ steps.ctx.outputs.pr_number }}", 10); + const prNumber = parseInt(process.env.PR_NUMBER, 10); const { data: pr } = await github.rest.pulls.get({ owner: context.repo.owner, repo: context.repo.repo, @@ -64,10 +73,12 @@ jobs: - name: Resolve author permission id: perm - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + env: + AUTHOR_LOGIN: ${{ steps.pr.outputs.author_login }} with: script: | - const login = "${{ steps.pr.outputs.author_login }}".toLowerCase(); + const login = (process.env.AUTHOR_LOGIN || '').toLowerCase(); let permission = 'none'; try { const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ @@ -89,10 +100,15 @@ jobs: - name: Skip trusted authors or drafts (unless forced) id: gate + env: + DRAFT: ${{ steps.pr.outputs.draft }} + FORCE: ${{ steps.ctx.outputs.force_all }} + SKIP_BY_PERMISSION: ${{ steps.perm.outputs.skip }} run: | - draft="${{ steps.pr.outputs.draft }}" - force="${{ steps.ctx.outputs.force_all }}" - skip_by_permission="${{ steps.perm.outputs.skip }}" + # Use environment variables for prior step outputs so untrusted values cannot alter shell syntax. + draft="${DRAFT:-}" + force="${FORCE:-}" + skip_by_permission="${SKIP_BY_PERMISSION:-}" if [ "$force" = "true" ]; then echo "skip=false" >> "$GITHUB_OUTPUT" else @@ -103,7 +119,7 @@ jobs: - name: Evaluate if: steps.gate.outputs.skip != 'true' id: eval - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 env: PRNUM: ${{ steps.pr.outputs.number }} TITLE: ${{ steps.pr.outputs.title }} @@ -212,7 +228,7 @@ jobs: - name: Sync label and comment if: always() && steps.ctx.outputs.pr_number != '' - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 env: SKIP: ${{ steps.gate.outputs.skip }} EVAL_COMPLETED: ${{ steps.eval.outputs.completed }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index eeb69a6..bd3bb82 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,13 +11,20 @@ on: type: string default: 'next' +# Default the token to no scopes; lint jobs only need read access for checkout. +permissions: {} + jobs: typos: runs-on: ubuntu-latest timeout-minutes: 5 + permissions: + contents: read steps: - - uses: actions/checkout@v4 - - uses: taiki-e/install-action@v2 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false + - uses: taiki-e/install-action@97a5807a604e12de3a13b52d868ebecaeeea757c # v2 with: tool: typos - run: make typos-check @@ -25,13 +32,17 @@ jobs: rustfmt: name: rustfmt runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - name: Rustup run: | rustup update --no-self-update nightly rustup +nightly component add rustfmt - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: save-if: ${{ github.event_name == 'push' && github.ref == format('refs/heads/{0}', inputs.target_branch) }} - name: Fmt @@ -40,13 +51,17 @@ jobs: clippy: name: clippy runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - name: Rustup run: | rustup update --no-self-update rustup component add clippy - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: save-if: ${{ github.event_name == 'push' && github.ref == format('refs/heads/{0}', inputs.target_branch) }} - name: Clippy @@ -55,9 +70,13 @@ jobs: toml: runs-on: ubuntu-latest timeout-minutes: 5 + permissions: + contents: read steps: - - uses: actions/checkout@v4 - - uses: taiki-e/install-action@v2 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false + - uses: taiki-e/install-action@97a5807a604e12de3a13b52d868ebecaeeea757c # v2 with: tool: taplo-cli - run: make toml-check @@ -65,9 +84,13 @@ jobs: workspace-lints: runs-on: ubuntu-latest timeout-minutes: 5 + permissions: + contents: read steps: - - uses: actions/checkout@v4 - - uses: taiki-e/install-action@v2 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false + - uses: taiki-e/install-action@97a5807a604e12de3a13b52d868ebecaeeea757c # v2 with: tool: cargo-workspace-lints - run: | @@ -76,11 +99,15 @@ jobs: doc: name: doc runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - name: Rustup run: rustup update --no-self-update - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: save-if: ${{ github.event_name == 'push' && github.ref == format('refs/heads/{0}', inputs.target_branch) }} - name: Build docs @@ -89,7 +116,11 @@ jobs: unused_deps: name: check for unused dependencies runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - name: machete - uses: bnjbvr/cargo-machete@main + uses: bnjbvr/cargo-machete@db87f5ee388b462ee8c4311742246d599b78a00a # main diff --git a/.github/workflows/publish-debian.yml b/.github/workflows/publish-debian.yml index dceb2bb..7d91dac 100644 --- a/.github/workflows/publish-debian.yml +++ b/.github/workflows/publish-debian.yml @@ -23,12 +23,37 @@ on: description: "Version to release (E.G. v0.10.0-rc.1, v0.10.0). Corresponding git tag must already exist." required: true type: string - + workflow_call: + inputs: + service: + description: "Name of service to publish" + required: true + type: string + crate_dir: + required: true + description: "Name of crate directory" + type: string + package: + required: true + description: "Name of packaging directory" + type: string + crate: + description: "Name of the binary crate to publish" + required: true + type: string + version: + description: "Version to release (E.G. v0.10.0-rc.1, v0.10.0). Corresponding git tag must already exist." + required: true + type: string + secrets: gh_token: description: 'Github token' required: true +# Default the token to no scopes; the publishing job opts into release-asset upload scope. +permissions: {} + jobs: publish: name: Publish ${{ matrix.arch }} Debian @@ -37,11 +62,14 @@ jobs: arch: [amd64, arm64] runs-on: labels: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} + permissions: + contents: write steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 + persist-credentials: false - name: Build and Publish Packages uses: ./.github/actions/debian diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index 5b3ae90..07a4eaa 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -42,17 +42,25 @@ on: description: 'AWS cache bucket' required: true +# Default the token to no scopes; each job opts into only the scopes it needs. +permissions: {} + jobs: publish: runs-on: labels: "ubuntu-latest" name: Publish ${{ inputs.component }} ${{ inputs.version }} + permissions: + contents: read + id-token: write + packages: write steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: ref: ${{ inputs.version }} fetch-depth: 0 + persist-credentials: false - name: Log in to the Container registry uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 @@ -62,23 +70,22 @@ jobs: password: ${{ secrets.gh_token }} - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4 with: aws-region: ${{ secrets.aws_region }} role-to-assume: ${{ secrets.aws_role }} role-session-name: GithubActionsSession - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 with: cache-binary: true - name: Build and push Docker image id: push - uses: docker/build-push-action@v5 + uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5 with: push: true - labels: ${{ steps.meta.outputs.labels }} file: ${{ inputs.dockerfile_path }} tags: ${{ inputs.registry }}/0xmiden/miden-${{ inputs.component }}:${{ inputs.version }} cache-from: type=s3,region=${{ secrets.aws_region }},bucket=${{ secrets.aws_cache_bucket }},name=miden-${{ inputs.component }} diff --git a/.github/workflows/release-plz-dry-run.yml b/.github/workflows/release-plz-dry-run.yml index 7c9b1af..6a0196c 100644 --- a/.github/workflows/release-plz-dry-run.yml +++ b/.github/workflows/release-plz-dry-run.yml @@ -18,22 +18,27 @@ on: description: 'Cargo registry token' required: true +# Default the token to no scopes; the dry run only opts into repository read access. +permissions: {} jobs: release-plz-dry-run-release: name: Release-plz dry-run runs-on: ubuntu-latest if: ${{ github.repository_owner == '0xMiden' }} + permissions: + contents: read steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 + persist-credentials: false - name: Update Rust toolchain run: | rustup update --no-self-update - name: Run release-plz - uses: release-plz/action@v0.5 + uses: release-plz/action@1528104d2ca23787631a1c1f022abb64b34c1e11 # v0.5 with: command: release --dry-run config: ${{ inputs.release_config }} diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml index 04d42f9..111dc97 100644 --- a/.github/workflows/release-plz.yml +++ b/.github/workflows/release-plz.yml @@ -18,23 +18,32 @@ on: description: 'Cargo registry token' required: true +# Default the token to no scopes; the release job opts into publication scopes below. +permissions: {} + jobs: release-plz-release: name: Release-plz release runs-on: ubuntu-latest if: ${{ github.repository_owner == '0xMiden' }} + permissions: + contents: write + pull-requests: write steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 ref: main + persist-credentials: false # Ensure the release tag refers to the latest commit on target ref. # Compare the commit SHA that triggered the workflow with the HEAD of the branch we just # checked out (main). - name: Verify release was triggered from the target ref HEAD + env: + TRIGGER_SHA: ${{ github.sha }} run: | - tag_sha="${{ github.sha }}" + tag_sha="$TRIGGER_SHA" main_sha="$(git rev-parse HEAD)" echo "Tag points to: $tag_sha" @@ -49,7 +58,7 @@ jobs: run: | rustup update --no-self-update - name: Run release-plz - uses: release-plz/action@v0.5 + uses: release-plz/action@1528104d2ca23787631a1c1f022abb64b34c1e11 # v0.5 with: command: release config: ${{ inputs.release_config }} diff --git a/.github/workflows/signed-commits.yml b/.github/workflows/signed-commits.yml index b05aca9..69bc30a 100644 --- a/.github/workflows/signed-commits.yml +++ b/.github/workflows/signed-commits.yml @@ -1,20 +1,26 @@ # Verifies that all commits in a PR are signed (GPG or SSH). # Caller responsibilities: # - Trigger on `pull_request_target`. -# - Grant `contents: read` and `issues: write`. +# - Grant `pull-requests: read` and `issues: write`. name: signed commits on: workflow_call: +# Default the token to no scopes; this check only reads PR commits and writes/removes issue comments. +permissions: {} + jobs: check-signed-commits: runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: read steps: - name: Check for unsigned commits id: check - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: script: | const prNumber = context.payload.pull_request.number; @@ -47,7 +53,7 @@ jobs: - name: Sync remediation comment if: always() && steps.check.outputs.pr_number != '' - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 env: COMPLETED: ${{ steps.check.outputs.completed }} UNSIGNED: ${{ steps.check.outputs.unsigned }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 55631b7..3c1c3b1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,19 +11,26 @@ on: type: string default: 'next' +# Default the token to no scopes; test jobs only need read access for checkout. +permissions: {} + jobs: test: name: test runs-on: ubuntu-latest timeout-minutes: 30 + permissions: + contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - name: Rustup run: rustup update --no-self-update - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: save-if: ${{ github.event_name == 'push' && github.ref == format('refs/heads/{0}', inputs.target_branch) }} - - uses: taiki-e/install-action@nextest + - uses: taiki-e/install-action@0dacf2f51ba412930ad0bbe41b20711980f5fe0f # nextest - name: Run tests run: make test @@ -31,11 +38,15 @@ jobs: name: Documentation tests runs-on: ubuntu-latest timeout-minutes: 30 + permissions: + contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - name: Rustup run: rustup update --no-self-update - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: save-if: ${{ github.event_name == 'push' && github.ref == format('refs/heads/{0}', inputs.target_branch) }} - name: Run doc tests diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 0000000..36733d0 --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,40 @@ +name: GitHub Actions Security Analysis + +on: + pull_request: + paths: + - ".github/actions/**" + - ".github/workflows/**" + push: + branches: ["main"] + paths: + - ".github/actions/**" + - ".github/workflows/**" + workflow_dispatch: + +# Default the token to no scopes; the zizmor job opts into read-only analysis access below. +permissions: {} + +jobs: + zizmor: + name: Run zizmor + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false + + - name: Run zizmor + uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2 + with: + inputs: ".github/workflows .github/actions" + # Avoid a GitHub Advanced Security dependency; this keeps the new check usable in private repos too. + advanced-security: false + # Gate high-severity issues now while medium advisory findings are handled without breaking callers. + min-severity: high + # Pin the scanner version so a hash-pinned action cannot silently install a different zizmor release. + version: v1.23.1