diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d9c4d20..dee3df2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,5 +1,12 @@ name: Publish Images +# The automatic path is CI -> Publish Images -> Deploy Delta, chained on +# workflow_run. That chain has a single point of failure outside our control: +# when GitHub throttles webhook delivery during an Actions incident, no +# workflow_run event fires and nothing publishes, so Deploy Delta has no image +# to deploy and main sits undeployed with no way to force it. workflow_dispatch +# goes through the REST API instead, which stays up independently, so this is +# the manual door for exactly that case. on: workflow_run: workflows: @@ -8,9 +15,14 @@ on: - main types: - completed + workflow_dispatch: + inputs: + sha: + description: Full commit SHA on main to build (defaults to the tip of main) + required: false concurrency: - group: publish-${{ github.event.workflow_run.head_sha }} + group: publish-${{ github.event.workflow_run.head_sha || inputs.sha || github.sha }} cancel-in-progress: false permissions: @@ -19,21 +31,53 @@ permissions: jobs: publish: + # Both paths publish only main. The automatic one gets that from the + # workflow_run fields; the manual one is pinned to the main ref here and + # then has its resolved SHA checked for ancestry below, so a dispatch + # cannot publish a commit that never landed on main. if: > - github.event.workflow_run.conclusion == 'success' && - github.event.workflow_run.name == 'CI' && - github.event.workflow_run.head_branch == 'main' && - github.event.workflow_run.event == 'push' + (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') || + ( + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.name == 'CI' && + github.event.workflow_run.head_branch == 'main' && + github.event.workflow_run.event == 'push' + ) runs-on: ubuntu-latest steps: - name: Resolve SHA id: sha + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - sha="${{ github.event.workflow_run.head_sha }}" + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + sha="${{ inputs.sha }}" + if [[ -z "$sha" ]]; then + sha="${{ github.sha }}" + fi + else + sha="${{ github.event.workflow_run.head_sha }}" + fi + if [[ ! "$sha" =~ ^[0-9a-fA-F]{40}$ ]]; then echo "invalid full commit SHA: $sha" >&2 exit 1 fi + + # A hand-supplied SHA is the one input here that is not already + # constrained to main, so confirm it is on main before we build an + # image Deploy Delta would treat as a production release. Comparing + # main to the commit reports "identical" when it is the tip and + # "behind" when it is an earlier commit on main; anything else means + # it lives on some other branch. + if [[ "${{ github.event_name }}" == "workflow_dispatch" && -n "${{ inputs.sha }}" ]]; then + status="$(gh api "repos/${GITHUB_REPOSITORY}/compare/main...${sha}" --jq .status)" + if [[ "$status" != "identical" && "$status" != "behind" ]]; then + echo "refusing to publish $sha: not a commit on main (compare status: $status)" >&2 + exit 1 + fi + fi + echo "value=$sha" >> "$GITHUB_OUTPUT" - uses: actions/checkout@v4