.github/workflows/ci.yml (4210B)
1 name: ci
2
3 on:
4 push:
5 branches: [main]
6 tags: ["v*"]
7 pull_request:
8 workflow_dispatch:
9
10 permissions:
11 contents: read
12
13 jobs:
14 test:
15 runs-on: ubuntu-latest
16 steps:
17 - uses: actions/checkout@v4
18
19 - name: shellcheck
20 run: sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck
21
22 - name: version strings must match the tag being released
23 if: startsWith(github.ref, 'refs/tags/v')
24 run: |
25 want="${GITHUB_REF_NAME}"
26 got=$(sed -n 's/^VERSION=${TT_VERSION:-\(v[0-9.]*\)}.*/\1/p' install.sh)
27 echo "tag=$want install.sh=$got"
28 [ "$want" = "$got" ] || { echo "::error::install.sh pins $got but the tag is $want"; exit 1; }
29 bad=$(grep -oE 'v1\.[0-9]+\.[0-9]+' install.sh README.md | grep -v ":$want$" || true)
30 [ -z "$bad" ] || { echo "::error::stale version strings:"; echo "$bad"; exit 1; }
31
32 # Builds both images for amd64 only and runs their offline self-checks.
33 # Everything runs with --network none, so CI never registers a WARP
34 # identity: hammering Cloudflare's registration endpoint from shared
35 # runner IPs is how an entire address range gets rate-limited.
36 - name: test
37 run: sh test.sh
38
39 publish:
40 needs: test
41 if: startsWith(github.ref, 'refs/tags/v')
42 runs-on: ubuntu-latest
43 permissions:
44 contents: write
45 packages: write
46 steps:
47 - uses: actions/checkout@v4
48 - uses: docker/setup-qemu-action@v3
49 - uses: docker/setup-buildx-action@v3
50 - uses: docker/login-action@v3
51 with:
52 registry: ghcr.io
53 username: ${{ github.actor }}
54 password: ${{ secrets.GITHUB_TOKEN }}
55
56 - id: meta
57 uses: docker/metadata-action@v5
58 with:
59 images: ghcr.io/${{ github.repository_owner }}/transtation
60 tags: |
61 type=semver,pattern={{version}}
62 type=semver,pattern=v{{version}}
63 type=raw,value=latest,enable={{is_default_branch}}
64 type=sha,format=long
65
66 - id: build
67 uses: docker/build-push-action@v6
68 with:
69 context: .
70 file: server/Dockerfile
71 platforms: linux/amd64,linux/arm64
72 push: true
73 tags: ${{ steps.meta.outputs.tags }}
74 labels: ${{ steps.meta.outputs.labels }}
75 # Provenance and SBOM attestations are off: they add a manifest list
76 # entry that some older clients choke on, and the compose file is
77 # already pinned by digest, which closes the mutable-tag hole for
78 # free. Turn them on when a downstream consumer asks for them.
79 provenance: false
80 sbom: false
81
82 # The digest goes into install.sh at release time so that the compose file
83 # pins the image by content, not by a mutable tag.
84 - name: digest
85 run: echo "transtation -> ${{ steps.build.outputs.digest }}" >> "$GITHUB_STEP_SUMMARY"
86
87 # The README tells people to check install.sh against a hash in the release
88 # notes, so the release notes have to exist and have to carry that hash.
89 - name: publish the release notes with the installer hash
90 env:
91 GH_TOKEN: ${{ github.token }}
92 run: |
93 sum=$(sha256sum install.sh | cut -d" " -f1)
94 {
95 echo "\`\`\`"
96 echo "$sum install.sh"
97 echo "\`\`\`"
98 echo
99 echo " curl -fsSLO https://raw.githubusercontent.com/${{ github.repository }}/${{ github.ref_name }}/install.sh"
100 echo " sha256sum install.sh # must match the line above"
101 echo " less install.sh"
102 echo " sudo sh install.sh"
103 echo
104 echo "Image: \`ghcr.io/${{ github.repository_owner }}/transtation:${{ github.ref_name }}\` (amd64, arm64). The client is a native install, not a container."
105 echo "The installer resolves the tag to a digest at install time and pins the compose file to it."
106 } > notes.md
107 gh release create "${{ github.ref_name }}" --title "${{ github.ref_name }}" --notes-file notes.md || \
108 gh release edit "${{ github.ref_name }}" --notes-file notes.md