|
| 1 | +# Builds the three FORA service images and publishes them to the GitHub |
| 2 | +# Container Registry. |
| 3 | +# |
| 4 | +# WHAT STARTS IT. A tag whose name begins with "v", pushed to this repository, |
| 5 | +# or a manual run from the Actions tab. Publishing source and publishing images |
| 6 | +# are deliberately separate acts: a source snapshot on its own builds nothing, |
| 7 | +# so a documentation-only update never reissues the images. |
| 8 | +# |
| 9 | +# A manual run must select a TAG as its ref, not a branch. The version comes from |
| 10 | +# the tag and there is nowhere else to get it; a branch produces no version at |
| 11 | +# all, and the first step below stops the run rather than letting it fail later |
| 12 | +# with something less obvious. |
| 13 | +# |
| 14 | +# AUTHENTICATION. The per-run GITHUB_TOKEN and nothing else. No personal access |
| 15 | +# token is created, stored or rotated for this workflow. |
| 16 | +# |
| 17 | +# TAGS. Exactly one per build: the version from the git tag with the leading "v" |
| 18 | +# removed. There is no "latest" and no moving major or minor pointer, so a |
| 19 | +# published tag names one build for ever. Production should pin by digest |
| 20 | +# (@sha256:...), which every image carries whether or not it is tagged. |
| 21 | +# |
| 22 | +# ARCHITECTURE. linux/amd64 only. The Broker and the Identity service are pure |
| 23 | +# Go and would need no more than the extra platform here, but the Exchange links |
| 24 | +# a C library for the ledger client, so an x86 runner cannot build its arm64 |
| 25 | +# variant. Adding ARM means an arm64 runner and a manifest merge. |
| 26 | +# |
| 27 | +# Third-party actions are pinned by commit rather than by tag, because whoever |
| 28 | +# owns an action can repoint its tags at any time. |
| 29 | + |
| 30 | +name: Publish container images |
| 31 | + |
| 32 | +on: |
| 33 | + push: |
| 34 | + tags: |
| 35 | + - "v*" |
| 36 | + workflow_dispatch: |
| 37 | + |
| 38 | +permissions: |
| 39 | + contents: read |
| 40 | + packages: write |
| 41 | + |
| 42 | +concurrency: |
| 43 | + # One publish per ref at a time, and never cancel a run that is under way. A |
| 44 | + # half-finished release leaves some of the three images published and some |
| 45 | + # not, which is worse than a slow queue. |
| 46 | + group: publish-images-${{ github.ref }} |
| 47 | + cancel-in-progress: false |
| 48 | + |
| 49 | +jobs: |
| 50 | + publish: |
| 51 | + runs-on: ubuntu-24.04 |
| 52 | + strategy: |
| 53 | + # The three build independently. One failure must not hide the other two |
| 54 | + # results, which is exactly what you need when a release breaks. |
| 55 | + fail-fast: false |
| 56 | + matrix: |
| 57 | + include: |
| 58 | + - service: exchange |
| 59 | + dockerfile: src/exchange/Dockerfile |
| 60 | + - service: broker |
| 61 | + dockerfile: src/broker/Dockerfile |
| 62 | + - service: identity |
| 63 | + dockerfile: src/identity/Dockerfile |
| 64 | + |
| 65 | + steps: |
| 66 | + - name: Check out the source |
| 67 | + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 |
| 68 | + |
| 69 | + - name: Set up Buildx |
| 70 | + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 |
| 71 | + |
| 72 | + - name: Sign in to the registry |
| 73 | + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 |
| 74 | + with: |
| 75 | + registry: ghcr.io |
| 76 | + username: ${{ github.actor }} |
| 77 | + password: ${{ secrets.GITHUB_TOKEN }} |
| 78 | + |
| 79 | + - name: Derive the image tag |
| 80 | + id: meta |
| 81 | + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 |
| 82 | + with: |
| 83 | + # Lower case throughout: the registry refuses a mixed-case name. |
| 84 | + images: ghcr.io/fora-protocol/${{ matrix.service }} |
| 85 | + # This list REPLACES the action's default rules. Keep it to the single |
| 86 | + # entry: a default rule would publish tags that move. |
| 87 | + tags: | |
| 88 | + type=semver,pattern={{version}} |
| 89 | + # Needed as well. The "latest" flavour defaults to "auto", which adds |
| 90 | + # a latest tag on its own as soon as the ref is a version tag. |
| 91 | + flavor: | |
| 92 | + latest=false |
| 93 | +
|
| 94 | + - name: Refuse a run that has no version to publish |
| 95 | + # A manual run can select any ref. On a branch the semver rule above |
| 96 | + # matches nothing, so the tag list comes out empty. Without this step the |
| 97 | + # build compiles all the way through and then fails inside the push, and |
| 98 | + # the message it fails with does not say "you picked a branch". |
| 99 | + # |
| 100 | + # The ref reaches the script through the environment, never through an |
| 101 | + # expression. GitHub substitutes an expression into the script TEXT |
| 102 | + # before bash reads it, so at that point a ref name is command text. |
| 103 | + # A ref name may hold $, backticks, parentheses, ; & and |. git does |
| 104 | + # reject some characters — spaces, backslashes, control characters, and |
| 105 | + # the revision syntax it needs for itself, : ? * [ ~ ^ — but not one of |
| 106 | + # those omissions closes this hole. The job holds a token with |
| 107 | + # packages: write for all three images. |
| 108 | + # |
| 109 | + # The message names no version. The tag is the only source of one, so an |
| 110 | + # example here would be a second place to edit at release time, and the |
| 111 | + # last one anybody would think to check. |
| 112 | + if: steps.meta.outputs.tags == '' |
| 113 | + env: |
| 114 | + TRIGGER_REF: ${{ github.ref }} |
| 115 | + run: | |
| 116 | + echo "::error::No version tag was derived from ${TRIGGER_REF}. The version comes from the tag name and nowhere else: run this workflow against a tag whose name is the version to publish, with a leading v." |
| 117 | + exit 1 |
| 118 | +
|
| 119 | + - name: Build and push |
| 120 | + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 |
| 121 | + with: |
| 122 | + # The repository root, because every service Dockerfile also copies |
| 123 | + # the shared internal/ tree. |
| 124 | + context: . |
| 125 | + file: ${{ matrix.dockerfile }} |
| 126 | + platforms: linux/amd64 |
| 127 | + push: true |
| 128 | + tags: ${{ steps.meta.outputs.tags }} |
| 129 | + # Only the two labels that cannot be known until the build runs. The |
| 130 | + # rest — source, licenses, title, description — are baked into each |
| 131 | + # Dockerfile, and passing the metadata action's full label set here |
| 132 | + # would overwrite the per-service title and description with this |
| 133 | + # repository's own name for all three images. There is no created |
| 134 | + # label because the image config already records a creation time. |
| 135 | + labels: | |
| 136 | + org.opencontainers.image.version=${{ steps.meta.outputs.version }} |
| 137 | + org.opencontainers.image.revision=${{ github.sha }} |
| 138 | + # Off on purpose. With provenance on, the pushed tag resolves to an |
| 139 | + # image index carrying an extra attestation entry, which is a |
| 140 | + # multi-platform manifest in everything but name. One platform is |
| 141 | + # published, so the tag should resolve straight to that one image. |
| 142 | + provenance: false |
| 143 | + # Without a cache every run recompiles Go from cold, several minutes |
| 144 | + # per service. The scope keeps the three from evicting each other. |
| 145 | + cache-from: type=gha,scope=${{ matrix.service }} |
| 146 | + cache-to: type=gha,mode=max,scope=${{ matrix.service }} |
0 commit comments