From c5040382e11bd3480dc5cf8d30252db9e7d98de1 Mon Sep 17 00:00:00 2001 From: laurentsimon <64505099+laurentsimon@users.noreply.github.com> Date: Mon, 26 Sep 2022 08:37:32 -0700 Subject: [PATCH] feat: Add npm builder workflow (#881) * Add workflow for npm builder * update privacy-check * update * update * update * update * yaml lint * update * update * linter --- .github/actions/checkout-node/action.yml | 65 ++++ .github/workflows/builder_go_slsa3.yml | 8 +- .github/workflows/builder_node_slsa3.yml | 383 +++++++++++++++++++++++ .github/workflows/pre-submit.actions.yml | 4 +- 4 files changed, 454 insertions(+), 6 deletions(-) create mode 100644 .github/actions/checkout-node/action.yml create mode 100644 .github/workflows/builder_node_slsa3.yml diff --git a/.github/actions/checkout-node/action.yml b/.github/actions/checkout-node/action.yml new file mode 100644 index 0000000000..858d397715 --- /dev/null +++ b/.github/actions/checkout-node/action.yml @@ -0,0 +1,65 @@ +name: "Checkout a repository for a Node project" +description: "Checkout and setup the environment for a Node project" +inputs: + repository: + description: "Repository name with owner." + required: false + # Same default as https://github.com/actions/checkout/blob/main/action.yml#L6. + default: ${{ github.repository }} + ref: + # Note: the logic is fairly involved https://github.com/actions/checkout/blob/main/src/ref-helper.ts, + # so we do not attempt to resolve it ourselves or provide a default value. We let the official `actions/checkout` + # do it for us. + description: "The branch, tag or SHA to checkout." + required: false + token: + description: "The token to use." + required: false + # Same default as https://github.com/actions/checkout/blob/main/action.yml#L24. + default: ${{ github.token }} + node-version: + description: "The Node version to use, as expected by https://github.com/actions/setup-node." + required: true + +runs: + using: "composite" + steps: + # Note: we could use a single block: + # `uses: actions/checkout + # with: + # ref: "${{ inputs.ref }}"` + # and it would work, because the ref field does not have a default + # value set https://github.com/actions/checkout/blob/main/action.yml#L7-L11. + # However, if this were to change in the future, we'd be setting an empty value + # when the developer has not defined it; and it would overwrite the default value + # set by the `actions/checkout`. Even if it is highly unlikely the `actions/checkout` team + # will set a default value in the future, we want to be sure it does not affect us if they do. + # This is why we use 2 blocks to call the `actions/checkout`: + # 1. if inputs.ref != '' + # 2. if inputs.ref == '' + - name: Checkout the repository with user ref + if: inputs.ref != '' + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3.0.2 + with: + fetch-depth: 1 + persist-credentials: false + repository: "${{ inputs.repository }}" + ref: "${{ inputs.ref }}" + token: "${{ inputs.token }}" + + - name: Checkout the repository with default ref + if: inputs.ref == '' + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # tag=v3.0.2 + with: + fetch-depth: 1 + persist-credentials: false + repository: "${{ inputs.repository }}" + token: "${{ inputs.token }}" + + - name: Verify checkout + uses: slsa-framework/slsa-github-generator/.github/actions/verify-checkout@e3220805577deb9d193f64e519abcb3b50851df5 + + - name: Set up Node environment + uses: actions/setup-node@2fddd8803e2f5c9604345a0b591c3020ee971a93 # tag=v3.4.1 + with: + node-version: "${{ inputs.node-version }}" diff --git a/.github/workflows/builder_go_slsa3.yml b/.github/workflows/builder_go_slsa3.yml index 929de7469d..4ee531ad3d 100644 --- a/.github/workflows/builder_go_slsa3.yml +++ b/.github/workflows/builder_go_slsa3.yml @@ -24,6 +24,10 @@ env: BUILDER_BINARY: slsa-builder-go-linux-amd64 # Name of the binary in the release assets. BUILDER_DIR: internal/builders/go # Source directory if we compile the builder. +defaults: + run: + shell: bash + ################################################################### # # # Input and output argument definitions # @@ -156,7 +160,6 @@ jobs: - name: Build dry project id: build-dry - shell: bash env: CONFIG_FILE: "${{ inputs.config-file }}" UNTRUSTED_ENVS: "${{ inputs.evaluated-envs }}" @@ -195,7 +198,6 @@ jobs: - name: Download dependencies env: UNTRUSTED_WORKING_DIR: "${{ needs.build-dry.outputs.go-working-dir }}" - shell: bash run: | set -euo pipefail @@ -211,7 +213,6 @@ jobs: - name: Build project id: build-gen - shell: bash env: CONFIG_FILE: "${{ inputs.config-file }}" UNTRUSTED_ENVS: "${{ inputs.evaluated-envs }}" @@ -262,7 +263,6 @@ jobs: - name: Create and sign provenance id: sign-prov - shell: bash env: UNTRUSTED_BINARY_NAME: "${{ needs.build-dry.outputs.go-binary-name }}" UNTRUSTED_BINARY_HASH: "${{ needs.build.outputs.go-binary-sha256 }}" diff --git a/.github/workflows/builder_node_slsa3.yml b/.github/workflows/builder_node_slsa3.yml new file mode 100644 index 0000000000..171ddfc0a7 --- /dev/null +++ b/.github/workflows/builder_node_slsa3.yml @@ -0,0 +1,383 @@ +# Copyright The GOSST team. +# +# 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. + +name: SLSA node builder + +permissions: + contents: read + +env: + # Builder. + BUILDER_BINARY: slsa-builder-node-linux-amd64 # Name of the binary in the release assets. + BUILDER_DIR: internal/builders/node # Source directory if we compile the builder. + +defaults: + run: + shell: bash + +################################################################### +# # +# Input and output argument definitions # +# # +################################################################### +on: + workflow_call: + secrets: + token: + description: > + Optional token. + + This argument is passed, unchanged, to `actions/node-setup`'s + as `token` parameter. + required: false + # Note: set to the same value as actions/node-setup https://github.com/actions/setup-node/blob/main/action.yml#L19. + default: ${{ github.token }} + # TODO(https://github.com/slsa-framework/slsa-github-generator/issues/890): support outputing the final tarball name. + inputs: + node-version: + description: > + The node version to use. + + This argument is passed, unchanged, to `actions/node-setup`'s + as `node-version` parameter. + required: true + type: string + # TODO(https://github.com/slsa-framework/slsa-github-generator/issues/888): support uploading assets to GitHub release. + directory: + description: "The directory to change to before running commands." + required: false + type: string + default: "" + private-repository: + description: "If true, private repositories can post to the public transparency log." + required: false + type: boolean + default: false + ci-arguments: + # TODO(https://github.com/slsa-framework/slsa-github-generator/issues/889): check whether we should filter out some arguments. + description: > + Optional list of arguments for `npm ci`, separated by a comma `,`. + + Example: `--arg1, --arg2`. + required: false + type: string + default: "" + run-scripts: + description: > + A list of scripts to run in order to build the package, separated by a comma `,`. + + Example: `script1, script2`. + + The scripts are run after `npm ci`. + The scripts are run using `npm run `, in the same + order as they appear in the list. + required: false + type: string + default: "" + publish-arguments: + description: > + Optional list of arguments to publish, separated by a comma `,`. + + Example: `--tag v1.2.3, --some-arg value`. + required: false + default: "" + type: string + registry-url: + description: > + Optional registry to publish to. + + This argument is passed, unchanged, to `actions/node-setup`'s + as `registry-url` parameter. + required: false + default: "" + type: string + scope: + description: > + Optional scope used to publish. + + This argument is passed, unchanged, to `actions/node-setup`'s + as `scope` parameter. + required: false + default: "" + type: string + always-auth: + description: > + Enable always-auth in nmprc. + + This argument is passed, unchanged, to `actions/node-setup`'s + as `always-auth` parameter. + required: false + type: boolean + # Note: same value as action https://github.com/actions/setup-node/blob/main/action.yml#L5. + default: false + + compile-builder: + description: "Build the builder from source. This increases build time by ~2m." + required: false + type: boolean + default: false + +jobs: + privacy-check: + runs-on: ubuntu-latest + steps: + - name: Check private repos + uses: slsa-framework/slsa-github-generator/.github/actions/privacy-check@a3c7a56c8749c2c423f01bbcfd063315efc07a22 + with: + error_message: "Repository is private. The workflow has halted in order to keep the repository name from being exposed in the public transparency log. Set 'private-repository' to override." + override: ${{ inputs.private-repository }} + + rng: + outputs: + value: ${{ steps.rng.outputs.random }} + runs-on: ubuntu-latest + steps: + - name: Generate random 16-byte value (32-char hex encoded) + id: rng + uses: slsa-framework/slsa-github-generator/.github/actions/rng@e3220805577deb9d193f64e519abcb3b50851df5 + + detect-env: + outputs: + repository: ${{ steps.detect.outputs.repository }} + ref: ${{ steps.detect.outputs.ref }} + runs-on: ubuntu-latest + permissions: + id-token: write # Needed to detect the current reusable repository and ref. + steps: + - name: Detect the builder ref + id: detect + uses: slsa-framework/slsa-github-generator/.github/actions/detect-workflow@bdd89e60dc5387d8f819bebc702987956bcd4913 # tag=v1.2.0 + + ################################################################### + # # + # Build the builder # + # # + ################################################################### + builder: + outputs: + node-builder-sha256: ${{ steps.generate.outputs.sha256 }} + runs-on: ubuntu-latest + needs: [privacy-check, detect-env, rng] + steps: + - name: Generate builder + id: generate + uses: slsa-framework/slsa-github-generator/.github/actions/generate-builder@e3220805577deb9d193f64e519abcb3b50851df5 + with: + repository: "${{ needs.detect-env.outputs.repository }}" + ref: "${{ needs.detect-env.outputs.ref }}" + go-version: 1.18 + # Note: This must be the non-randomized binary name, so that it can be downloaded from the release assets. + binary: "${{ env.BUILDER_BINARY }}" + compile-builder: "${{ inputs.compile-builder }}" + directory: "${{ env.BUILDER_DIR }}" + + - name: Upload builder + uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # tag=v3.1.0 + with: + name: "${{ env.BUILDER_BINARY }}-${{ needs.rng.outputs.value }}" + path: "${{ env.BUILDER_BINARY }}" + if-no-files-found: error + retention-days: 5 + + ################################################################### + # # + # Build the project # + # # + ################################################################### + build: + outputs: + node-tarball-sha256: ${{ steps.upload.outputs.sha256 }} + node-tarball-name: ${{ steps.tarball.outputs.filename }} + runs-on: ubuntu-latest + needs: [privacy-check, builder, rng] + steps: + - name: Checkout the Node repository + uses: slsa-framework/slsa-github-generator/.github/actions/checkout-node # TODO: set a pin or use at head? + with: + node-version: ${{ inputs.node-version }} + scope: ${{ inputs.scope }} + token: ${{ inputs.token }} + always-auth: ${{ inputs.always-auth }} + registry-url: ${{ inputs.registry-url }} + cache: 'npm' + + - name: Download builder + uses: slsa-framework/slsa-github-generator/.github/actions/secure-download-artifact@e3220805577deb9d193f64e519abcb3b50851df5 + with: + name: "${{ env.BUILDER_BINARY }}-${{ needs.rng.outputs.value }}" + path: "${{ env.BUILDER_BINARY }}" + sha256: "${{ needs.builder.outputs.node-builder-sha256 }}" + set-executable: true + + - name: Download dependencies + env: + UNTRUSTED_CI_ARGUMENTS: "${{ inputs.ci-arguments }}" + UNTRUSTED_DIR: ${{ inputs.directory }}" + run: | + set -euo pipefail + + # npm ci + ./"$BUILDER_BINARY" ci \ + --ci-arguments "$UNTRUSTED_CI_ARGUMENTS" \ + --directory "$UNTRUSTED_DIR" + + # TODO(hermeticity) Enable OS-level hermeticity. + + - name: Build project + env: + UNTRUSTED_RUN_SCRIPTS: "${{ inputs.run-scripts }}" + UNTRUSTED_DIR: ${{ inputs.directory }}" + run: | + set -euo pipefail + + # npm run + ./"$BUILDER_BINARY" run \ + --run-scripts "$UNTRUSTED_RUN_SCRIPTS" \ + --directory "$UNTRUSTED_DIR" + + - name: Create tarball + id: tarball + env: + UNTRUSTED_DIR: ${{ inputs.directory }}" + run: | + set -euo pipefail + + # TODO: only run if non-empty. + # Note: pack-destination only supported version 7.x above. + # https://docs.npmjs.com/cli/v7/commands/npm-pack. + # This outputs a .tgz. Before running this command, let's record the .tgz + # files and their hashes, so that we can identify the new file without the need to parse + # the manifest.json. + # echo "npm pack --pack-destination="./out" + ./"$BUILDER_BINARY" pack \ + --directory "$UNTRUSTED_DIR" + + # cp output into upper folder to make the tarball accessible to + # next step. + echo '::set-output name=filename::$TARBALL' + + - name: Upload generated tarball + id: upload + uses: slsa-framework/slsa-github-generator/.github/actions/secure-upload-artifact@e3220805577deb9d193f64e519abcb3b50851df5 + with: + name: "${{ steps.tarball.outputs.filename }}" + path: "${{ steps.tarball.outputs.filename }}" + + ################################################################### + # # + # Generate the SLSA provenance # + # # + ################################################################### + provenance: + runs-on: ubuntu-latest + needs: [builder, build, rng] + permissions: + id-token: write # Needed to create an OIDC token for keyless signing. + contents: read + actions: read # Needed to read workflow info. + outputs: + node-provenance-name: ${{ steps.sign-prov.outputs.signed-provenance-name }} + node-provenance-sha256: ${{ steps.sign-prov.outputs.signed-provenance-sha256 }} + steps: + - name: Download builder + uses: slsa-framework/slsa-github-generator/.github/actions/secure-download-artifact@e3220805577deb9d193f64e519abcb3b50851df5 + with: + name: "${{ env.BUILDER_BINARY }}-${{ needs.rng.outputs.value }}" + path: "${{ env.BUILDER_BINARY }}" + sha256: "${{ needs.builder.outputs.node-builder-sha256 }}" + set-executable: true + + - name: Create and sign provenance + id: sign-prov + env: + UNTRUSTED_TARBALL_NAME: "${{ needs.build.outputs.node-tarball-name }}" + UNTRUSTED_TARBALL_HASH: "${{ needs.build.outputs.node-tarball-sha256 }}" + UNTRUSTED_CI_ARGUMENTS: "${{ inputs.ci-arguments }}" + UNTRUSTED_RUN_SCRIPTS: "${{ inputs.run-scripts }}" + UNTRUSTED_DIR: ${{ inputs.directory }}" + GITHUB_CONTEXT: "${{ toJSON(github) }}" + run: | + set -euo pipefail + + echo "provenance generator is $BUILDER_BINARY" + + # Create and sign provenance + # This sets signed-provenance-name to the name of the signed DSSE envelope. + # Note: this command outputs the sha256 of the signed provenance, so the upload + # in the next step need not be secure-artifact-upload. + ./"$BUILDER_BINARY" provenance \ + --tarball-name "$UNTRUSTED_TARBALL_NAME" \ + --digest "$UNTRUSTED_TARBALL_HASH" \ + --ci-arguments "$UNTRUSTED_CI_ARGUMENTS" \ + --run-scripts "$UNTRUSTED_RUN_SCRIPTS" \ + --directory "$UNTRUSTED_DIR" + + - name: Upload the signed provenance + uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # tag=v3.1.0 + with: + name: "${{ steps.sign-prov.outputs.signed-provenance-name }}" + path: "${{ steps.sign-prov.outputs.signed-provenance-name }}" + if-no-files-found: error + retention-days: 5 + + ################################################################### + # # + # Publish the package and its provenance # + # # + ################################################################### + publish: + runs-on: ubuntu-latest + needs: [build, provenance] + steps: + - name: Checkout the Node repository + uses: slsa-framework/slsa-github-generator/.github/actions/checkout-node # TODO: set a pin or use at head? + with: + node-version: ${{ inputs.node-version }} + scope: ${{ inputs.scope }} + token: ${{ inputs.token }} + always-auth: ${{ inputs.always-auth }} + registry-url: ${{ inputs.registry-url }} + + - name: Download binary + uses: slsa-framework/slsa-github-generator/.github/actions/secure-download-artifact@e3220805577deb9d193f64e519abcb3b50851df5 + with: + name: "${{ needs.build.outputs.node-tarball-name }}" + path: "${{ needs.build.outputs.node-tarball-name }}" + sha256: "${{ needs.build.outputs.node-tarball-sha256 }}" + + - name: Download provenance + uses: slsa-framework/slsa-github-generator/.github/actions/secure-download-artifact@e3220805577deb9d193f64e519abcb3b50851df5 + with: + name: "${{ needs.provenance.outputs.node-provenance-name }}" + path: "${{ needs.provenance.outputs.node-provenance-name }}" + sha256: "${{ needs.provenance.outputs.node-provenance-sha256 }}" + + - name: Download builder + uses: slsa-framework/slsa-github-generator/.github/actions/secure-download-artifact@e3220805577deb9d193f64e519abcb3b50851df5 + with: + name: "${{ env.BUILDER_BINARY }}-${{ needs.rng.outputs.value }}" + path: "${{ env.BUILDER_BINARY }}" + sha256: "${{ needs.builder.outputs.go-builder-sha256 }}" + set-executable: true + + - name: Publish + env: + UNTRUSTED_PUBLISH_ARGUMENTS: "${{ inputs.publish-arguments }}" + run: | + set -euo pipefail + + # echo "npm publish ${{ inputs.publish-arguments }}" + ./"$BUILDER_BINARY" publish \ + --publish-arguments "$UNTRUSTED_PUBLISH_ARGUMENTS" \ + --directory "$UNTRUSTED_DIR" diff --git a/.github/workflows/pre-submit.actions.yml b/.github/workflows/pre-submit.actions.yml index 25a5da2ede..ca5978c952 100644 --- a/.github/workflows/pre-submit.actions.yml +++ b/.github/workflows/pre-submit.actions.yml @@ -21,8 +21,8 @@ jobs: # See reasoning in ./github/actions/README.md # Split the command to ignore the `1` error `grep` returns when there is no match. - results=$(grep -r --include='*.yml' --include='*.yaml' -e 'actions/checkout@\|actions/checkout-go@' .github/actions/* || true) - results=$(grep -v 'checkout-go\|generate-builder' <<<"$results" || true) + results=$(grep -r --include='*.yml' --include='*.yaml' -e 'actions/checkout@\|actions/checkout-go@\|actions/checkout-node@' .github/actions/* || true) + results=$(grep -v 'checkout-go\|checkout-node\|generate-builder' <<<"$results" || true) if [[ "$results" != "" ]]; then echo "Some Actions are using 'actions/checkout'" echo "$results"