action.yaml 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. name: 'Sign Container Image'
  2. description: >-
  3. Sign a container image and attest SLSA v1 build provenance using cosign
  4. keyless (Sigstore) with GitHub Actions OIDC. Callers must check out the
  5. source tree (e.g. `actions/checkout`) before invoking this action — the
  6. built commit is resolved with `git rev-parse HEAD` so the attestation
  7. records the revision that was actually built rather than `github.sha`,
  8. which on a tag push reflects the tag-pointed commit and may differ from
  9. the branch tip the release workflow checks out. The calling job must
  10. have `id-token: write`.
  11. inputs:
  12. image:
  13. description: 'Full image reference (registry/repo:tag) to sign by digest.'
  14. required: true
  15. workflow-path:
  16. description: 'Path to the workflow file (repo-relative) that triggered the build.'
  17. required: true
  18. run-started-at:
  19. description: >-
  20. ISO-8601 workflow run start time (typically
  21. `github.run_started_at` from the caller workflow). Recorded as
  22. `runDetails.metadata.startedOn` in the SLSA provenance predicate.
  23. If empty, the action falls back to the time at which it began
  24. executing — `github.run_started_at` is reported empty in some
  25. edge cases and `required: true` on a composite-action input
  26. does not reject empty strings.
  27. required: true
  28. runs:
  29. using: "composite"
  30. steps:
  31. - name: Capture fallback start timestamp
  32. id: start
  33. shell: bash
  34. run: |
  35. echo "STARTED_ON=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "$GITHUB_OUTPUT"
  36. - name: Install cosign
  37. uses: sigstore/cosign-installer@v3
  38. - name: Install crane
  39. uses: imjasonh/setup-crane@v0.5
  40. - name: Resolve image digest
  41. id: digest
  42. shell: bash
  43. env:
  44. IMAGE: ${{ inputs.image }}
  45. run: |
  46. set -euo pipefail
  47. DIGEST="$(crane digest "${IMAGE}")"
  48. REPO="${IMAGE%:*}"
  49. REF="${REPO}@${DIGEST}"
  50. echo "Resolved ${IMAGE} -> ${REF}"
  51. echo "REF=${REF}" >> "$GITHUB_OUTPUT"
  52. echo "DIGEST=${DIGEST}" >> "$GITHUB_OUTPUT"
  53. - name: Sign image with cosign (keyless)
  54. shell: bash
  55. env:
  56. REF: ${{ steps.digest.outputs.REF }}
  57. run: |
  58. set -euo pipefail
  59. cosign sign --yes "${REF}"
  60. - name: Generate SLSA v1 provenance predicate
  61. shell: bash
  62. env:
  63. WORKFLOW_PATH: ${{ inputs.workflow-path }}
  64. STARTED_ON: ${{ inputs.run-started-at }}
  65. FALLBACK_STARTED_ON: ${{ steps.start.outputs.STARTED_ON }}
  66. run: |
  67. set -euo pipefail
  68. if [[ -z "${STARTED_ON:-}" ]]; then
  69. STARTED_ON="$FALLBACK_STARTED_ON"
  70. fi
  71. RESOLVED_GIT_COMMIT="$(git rev-parse HEAD)"
  72. export RESOLVED_GIT_COMMIT STARTED_ON
  73. python3 - <<'PY' > predicate.json
  74. import json
  75. import os
  76. repo = os.environ["GITHUB_REPOSITORY"]
  77. commit = os.environ["RESOLVED_GIT_COMMIT"]
  78. run_id = os.environ["GITHUB_RUN_ID"]
  79. run_attempt = os.environ["GITHUB_RUN_ATTEMPT"]
  80. predicate = {
  81. "buildDefinition": {
  82. "buildType": "https://github.com/opencost/opencost/build/workflow@v1",
  83. "externalParameters": {
  84. "workflow": {
  85. "ref": os.environ["GITHUB_REF"],
  86. "repository": f"https://github.com/{repo}",
  87. "path": os.environ["WORKFLOW_PATH"],
  88. }
  89. },
  90. "internalParameters": {},
  91. "resolvedDependencies": [
  92. {
  93. "uri": f"git+https://github.com/{repo}@{commit}",
  94. "digest": {"gitCommit": commit},
  95. }
  96. ],
  97. },
  98. "runDetails": {
  99. "builder": {
  100. "id": f"https://github.com/{repo}/actions/runs/{run_id}",
  101. },
  102. "metadata": {
  103. "invocationId": f"https://github.com/{repo}/actions/runs/{run_id}/attempts/{run_attempt}",
  104. "startedOn": os.environ["STARTED_ON"],
  105. },
  106. },
  107. }
  108. print(json.dumps(predicate, indent=2))
  109. PY
  110. - name: Attest SLSA provenance with cosign
  111. shell: bash
  112. env:
  113. REF: ${{ steps.digest.outputs.REF }}
  114. run: |
  115. set -euo pipefail
  116. cosign attest --yes \
  117. --predicate predicate.json \
  118. --type slsaprovenance1 \
  119. "${REF}"