2
0

action.yaml 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  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. echo "inputs.run-started-at is empty; using fallback ${FALLBACK_STARTED_ON}"
  70. STARTED_ON="$FALLBACK_STARTED_ON"
  71. fi
  72. if [[ -z "${STARTED_ON:-}" ]]; then
  73. echo "::error::startedOn resolved empty after fallback; predicate would be unparseable"
  74. exit 1
  75. fi
  76. echo "Using startedOn=${STARTED_ON}"
  77. RESOLVED_GIT_COMMIT="$(git rev-parse HEAD)"
  78. export RESOLVED_GIT_COMMIT STARTED_ON
  79. python3 - <<'PY' > predicate.json
  80. import json
  81. import os
  82. repo = os.environ["GITHUB_REPOSITORY"]
  83. commit = os.environ["RESOLVED_GIT_COMMIT"]
  84. run_id = os.environ["GITHUB_RUN_ID"]
  85. run_attempt = os.environ["GITHUB_RUN_ATTEMPT"]
  86. predicate = {
  87. "buildDefinition": {
  88. "buildType": "https://github.com/opencost/opencost/build/workflow@v1",
  89. "externalParameters": {
  90. "workflow": {
  91. "ref": os.environ["GITHUB_REF"],
  92. "repository": f"https://github.com/{repo}",
  93. "path": os.environ["WORKFLOW_PATH"],
  94. }
  95. },
  96. "internalParameters": {},
  97. "resolvedDependencies": [
  98. {
  99. "uri": f"git+https://github.com/{repo}@{commit}",
  100. "digest": {"gitCommit": commit},
  101. }
  102. ],
  103. },
  104. "runDetails": {
  105. "builder": {
  106. "id": f"https://github.com/{repo}/actions/runs/{run_id}",
  107. },
  108. "metadata": {
  109. "invocationId": f"https://github.com/{repo}/actions/runs/{run_id}/attempts/{run_attempt}",
  110. "startedOn": os.environ["STARTED_ON"],
  111. },
  112. },
  113. }
  114. print(json.dumps(predicate, indent=2))
  115. PY
  116. - name: Attest SLSA provenance with cosign
  117. shell: bash
  118. env:
  119. REF: ${{ steps.digest.outputs.REF }}
  120. run: |
  121. set -euo pipefail
  122. cosign attest --yes \
  123. --predicate predicate.json \
  124. --type slsaprovenance1 \
  125. "${REF}"