diff --git a/.github/workflows/verify-release-pointer.yml b/.github/workflows/verify-release-pointer.yml new file mode 100644 index 0000000..5c1de64 --- /dev/null +++ b/.github/workflows/verify-release-pointer.yml @@ -0,0 +1,88 @@ +name: Verify release pointer + +# Validates edits to version.yaml — the release pointer that selects the +# published image to deploy. Runs on every pull request (no paths filter) so it +# always reports a status; the job itself no-ops when version.yaml is untouched. +on: pull_request + +permissions: + contents: read + id-token: write + +env: + AWS_REGION: eu-central-1 + ECR_REPOSITORY: wallets-list + # Read-only ECR role, assumable only from pull_request events of this + # repository. Fork PRs cannot mint the OIDC token, so pointer changes from + # forks fail closed and must be re-raised by a maintainer. + ROLE_ARN: arn:aws:iam::990678687129:role/gha-ecr-read-wallets-list + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # full history for ancestry checks + + - name: Detect version.yaml change + id: changed + env: + BASE_REF: ${{ github.base_ref }} # pass via env, never inline in run: + run: | + git fetch --no-tags origin "$BASE_REF" + if [ -n "$(git diff --name-only "origin/$BASE_REF...HEAD" -- version.yaml)" ]; then + echo "changed=true" >> "$GITHUB_OUTPUT" + else + echo "version.yaml untouched; nothing to verify." + echo "changed=false" >> "$GITHUB_OUTPUT" + fi + + - name: Validate schema + if: steps.changed.outputs.changed == 'true' + run: | + # Exactly one top-level key `release`, with exactly `branch` + `sha`. + KEYS=$(yq -r '. | keys | join(",")' version.yaml) + [ "$KEYS" = "release" ] || { echo "::error::version.yaml must have exactly one top-level key 'release' (got: $KEYS)"; exit 1; } + RKEYS=$(yq -r '.release | keys | sort | join(",")' version.yaml) + [ "$RKEYS" = "branch,sha" ] || { echo "::error::release must have exactly 'branch' and 'sha' (got: $RKEYS)"; exit 1; } + + BRANCH=$(yq -r '.release.branch' version.yaml) + SHA=$(yq -r '.release.sha' version.yaml) + echo "$BRANCH" | grep -Eq '^(main|release/.+)$' || { echo "::error::release.branch must match ^(main|release/.+)$ (got: $BRANCH)"; exit 1; } + echo "$SHA" | grep -Eq '^[0-9a-f]{7,40}$' || { echo "::error::release.sha must be a 7-40 char hex commit id (got: $SHA)"; exit 1; } + + # Export for downstream steps. + { + echo "RELEASE_BRANCH=$BRANCH" + echo "RELEASE_SHA=$SHA" + } >> "$GITHUB_ENV" + + - name: Verify provenance + if: steps.changed.outputs.changed == 'true' + run: | + # The pointed sha must resolve to a commit AND be reachable from its branch. + git rev-parse --verify "${RELEASE_SHA}^{commit}" >/dev/null 2>&1 \ + || { echo "::error::sha not reachable from branch"; exit 1; } + git fetch --no-tags origin "${RELEASE_BRANCH}" + git merge-base --is-ancestor "$RELEASE_SHA" "origin/${RELEASE_BRANCH}" \ + || { echo "::error::sha not reachable from branch"; exit 1; } + + - name: Configure AWS credentials + if: steps.changed.outputs.changed == 'true' + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ env.ROLE_ARN }} + aws-region: ${{ env.AWS_REGION }} + + - name: Verify published image exists + if: steps.changed.outputs.changed == 'true' + run: | + # Load-bearing gate: the release pointer must reference an image that + # has actually been published. Tag is the exact sha string from the file. + aws ecr describe-images \ + --repository-name "$ECR_REPOSITORY" \ + --image-ids imageTag="$RELEASE_SHA" >/dev/null 2>&1 \ + || { echo "::error::no published image for this sha"; exit 1; } + echo "Published image found for $RELEASE_SHA."