mirror of
https://github.com/mudler/LocalAI.git
synced 2026-10-28 09:08:54 -04:00
ci(addons): publish every add-on to one repository (#12881)
All add-ons now publish to ADDON_OCI_REPOSITORY as tags of one repository. Each tag starts with the add-on id: <id>-sha-<short sha> for a commit, <id>-<version> for a release, <id>-development and <id>-latest for the channel aliases. The publish step, the alias promotion and the catalog renderer add the prefix, so the per-add-on repository step of the workflow is gone. A new addonpkg target command prints the reference an add-on publishes to without a network request. The workflow policy now refuses a derived repository. The host parser already reads a tag reference and a repository with a nested path, so both the new form and the old per-repository form install. A test covers each. The artifact fields in gallery/addons/index.yaml were changed by hand to the tag form. The digests are unchanged and stay until the add-ons are published again. Assisted-by: Claude:claude-sonnet-5-5 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
1 parent
34306b852d
commit
b8f5453cc1
10 files changed
+222
-136
No files matched your search
@@ -13,12 +13,14 @@ name: 'add-on packages'
|
||||
# push of a v<semver> tag prepare the immutable version, move no alias
|
||||
# release published promote latest, for a stable non-draft release only
|
||||
#
|
||||
# Every add-on has its own platform file (addons/<id>/package/platforms.yaml),
|
||||
# its own package legs and its own repository. Tables publishes to
|
||||
# ADDON_OCI_REPOSITORY, which is the repository <namespace>/tables. Each other
|
||||
# add-on publishes to <namespace>/<id>, where the namespace is
|
||||
# ADDON_OCI_REPOSITORY without its last path element. A publishing event fails
|
||||
# for an add-on until one of its platforms is enabled.
|
||||
# Every add-on has its own platform file (addons/<id>/package/platforms.yaml)
|
||||
# and its own package legs. All add-ons publish to one OCI repository, the
|
||||
# value of ADDON_OCI_REPOSITORY (the add-on registry organization). Each
|
||||
# add-on is a set of tags of that repository, prefixed with its id:
|
||||
# <id>-sha-<short sha> for a commit, <id>-<version> for a release and
|
||||
# <id>-development or <id>-latest for a channel alias. Signatures and digests
|
||||
# are per image, so add-ons never collide. A publishing event fails for an
|
||||
# add-on until one of its platforms is enabled.
|
||||
#
|
||||
# Aliases move only in the promote job, after every platform leg passed, the
|
||||
# index was pushed by digest, signed and verified.
|
||||
@@ -148,6 +150,10 @@ jobs:
|
||||
echo "ADDON_OCI_REPOSITORY is not configured; refusing to publish" >&2
|
||||
exit 1
|
||||
fi
|
||||
case "$ADDON_OCI_REPOSITORY" in
|
||||
*/*) ;;
|
||||
*) echo "ADDON_OCI_REPOSITORY needs a namespace and a name" >&2; exit 1 ;;
|
||||
esac
|
||||
fi
|
||||
# One set of legs for each add-on, from its own platform file. Only
|
||||
# the "no enabled platform" answer is tolerated, and only on an event
|
||||
@@ -253,7 +259,7 @@ jobs:
|
||||
matrix:
|
||||
addon: ${{ fromJSON(needs.plan.outputs.addons) }}
|
||||
env:
|
||||
ADDON_OCI_BASE: ${{ vars.ADDON_OCI_REPOSITORY }}
|
||||
ADDON_OCI_REPOSITORY: ${{ vars.ADDON_OCI_REPOSITORY }}
|
||||
ADDON: ${{ matrix.addon }}
|
||||
# cosign gates --registry-referrers-mode=oci-1-1 behind this flag.
|
||||
COSIGN_EXPERIMENTAL: '1'
|
||||
@@ -264,22 +270,6 @@ jobs:
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- name: Choose the repository of this add-on
|
||||
# Tables publishes to the configured repository. Every other add-on
|
||||
# publishes to <namespace>/<id>. The later steps read
|
||||
# ADDON_OCI_REPOSITORY.
|
||||
run: |
|
||||
set -eu
|
||||
case "$ADDON_OCI_BASE" in
|
||||
*/*) ;;
|
||||
*) echo "ADDON_OCI_REPOSITORY needs a namespace and a name" >&2; exit 1 ;;
|
||||
esac
|
||||
if [ "$ADDON" = tables ]; then
|
||||
repo="$ADDON_OCI_BASE"
|
||||
else
|
||||
repo="${ADDON_OCI_BASE%/*}/$ADDON"
|
||||
fi
|
||||
echo "ADDON_OCI_REPOSITORY=$repo" >> "$GITHUB_ENV"
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: layout-${{ matrix.addon }}-*
|
||||
@@ -305,6 +295,7 @@ jobs:
|
||||
IMMUTABLE: ${{ needs.plan.outputs.immutable }}
|
||||
run: |
|
||||
set -eu
|
||||
go run ./scripts/addons/cmd/addonpkg target --repository "$ADDON_OCI_REPOSITORY" --addon "$ADDON" --tag "$IMMUTABLE"
|
||||
go run ./scripts/addons/cmd/addonpkg publish --config "addons/$ADDON/package/platforms.yaml" \
|
||||
--layout merged --repository "$ADDON_OCI_REPOSITORY" --tag "$IMMUTABLE" --adopt-existing | tee push.txt
|
||||
echo "digest=$(sed -n 's/^digest=//p' push.txt)" >> "$GITHUB_OUTPUT"
|
||||
@@ -369,7 +360,7 @@ jobs:
|
||||
matrix:
|
||||
addon: ${{ fromJSON(needs.plan.outputs.addons) }}
|
||||
env:
|
||||
ADDON_OCI_BASE: ${{ vars.ADDON_OCI_REPOSITORY }}
|
||||
ADDON_OCI_REPOSITORY: ${{ vars.ADDON_OCI_REPOSITORY }}
|
||||
ADDON: ${{ matrix.addon }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
@@ -379,21 +370,6 @@ jobs:
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- name: Choose the repository of this add-on
|
||||
# The same rule as in publish: Tables keeps the configured repository,
|
||||
# every other add-on uses <namespace>/<id>.
|
||||
run: |
|
||||
set -eu
|
||||
case "$ADDON_OCI_BASE" in
|
||||
*/*) ;;
|
||||
*) echo "ADDON_OCI_REPOSITORY needs a namespace and a name" >&2; exit 1 ;;
|
||||
esac
|
||||
if [ "$ADDON" = tables ]; then
|
||||
repo="$ADDON_OCI_BASE"
|
||||
else
|
||||
repo="${ADDON_OCI_BASE%/*}/$ADDON"
|
||||
fi
|
||||
echo "ADDON_OCI_REPOSITORY=$repo" >> "$GITHUB_ENV"
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: digest-${{ matrix.addon }}
|
||||
@@ -420,7 +396,7 @@ jobs:
|
||||
esac
|
||||
for alias in $(echo "$ALIASES" | tr ',' ' '); do
|
||||
go run ./scripts/addons/cmd/addonpkg promote \
|
||||
--repository "$ADDON_OCI_REPOSITORY" --alias "$alias" \
|
||||
--repository "$ADDON_OCI_REPOSITORY" --addon "$ADDON" --alias "$alias" \
|
||||
--digest "$DIGEST" --version "$VERSION" --revision "$SHA" \
|
||||
--ref "$REF" --event "$EVENT" --git-dir .git
|
||||
done
|
||||
@@ -296,6 +296,47 @@ var _ = Describe("distribution adapters", func() {
|
||||
Expect(e).To(HaveOccurred())
|
||||
Expect(externalRequests.Load()).To(BeZero())
|
||||
})
|
||||
// The shared add-on repository names an add-on by a tag prefixed with its
|
||||
// id (oci://<repo>:<id>-sha-<short sha>). The reference is pinned by
|
||||
// digest, so the tag form decides nothing but the parse. The old
|
||||
// repository-per-add-on form still parses and installs.
|
||||
for _, form := range []struct{ title, path string }{
|
||||
{"the shared repository with an id-prefixed tag", "/local-ai-addons:sample-sha-0123456789ab"},
|
||||
{"the old repository per add-on", "/local-ai-addons/sample:sha-0123456789ab"},
|
||||
} {
|
||||
form := form
|
||||
It("installs from "+form.title, func() {
|
||||
m, e := os.ReadFile("../../../../tests/addons/fixtures/packages/manifest.json")
|
||||
Expect(e).NotTo(HaveOccurred())
|
||||
data := archive(nil, m)
|
||||
layer, e := tarball.LayerFromReader(bytes.NewReader(data))
|
||||
Expect(e).NotTo(HaveOccurred())
|
||||
img, e := mutate.AppendLayers(empty.Image, layer)
|
||||
Expect(e).NotTo(HaveOccurred())
|
||||
cfg, e := img.ConfigFile()
|
||||
Expect(e).NotTo(HaveOccurred())
|
||||
cfg.OS = "linux"
|
||||
cfg.Architecture = "amd64"
|
||||
img, e = mutate.ConfigFile(img, cfg)
|
||||
Expect(e).NotTo(HaveOccurred())
|
||||
server := httptest.NewServer(registry.New())
|
||||
DeferCleanup(server.Close)
|
||||
ref, e := name.ParseReference(strings.TrimPrefix(server.URL, "http://") + form.path)
|
||||
Expect(e).NotTo(HaveOccurred())
|
||||
Expect(remote.Write(ref, img)).To(Succeed())
|
||||
digest, e := img.Digest()
|
||||
Expect(e).NotTo(HaveOccurred())
|
||||
i, r := fixture(data, func(r *catalog.Release, s *catalog.Source) {
|
||||
r.Artifact = "oci://" + ref.Name()
|
||||
r.Digest = digest.String()
|
||||
})
|
||||
staged, e := i.Stage(context.Background(), r, nil)
|
||||
Expect(e).NotTo(HaveOccurred())
|
||||
receipt, e := i.Commit(context.Background(), staged)
|
||||
Expect(e).NotTo(HaveOccurred())
|
||||
Expect(receipt.Digest).To(Equal(digest.String()))
|
||||
})
|
||||
}
|
||||
It("rejects downloaded digest mismatch redirects cancellation and file count limits", func() {
|
||||
m, e := os.ReadFile("../../../../tests/addons/fixtures/packages/manifest.json")
|
||||
Expect(e).NotTo(HaveOccurred())
|
||||
|
||||
@@ -3222,8 +3222,8 @@ platforms of that add-on. A platform is shipped only
|
||||
while `enabled: true`, and it needs a native runner and the `executable-start` native
|
||||
check. Until a platform's native check passes on a runner of that platform, it stays
|
||||
listed with `enabled: false`. The workflow fails closed on an empty matrix and on a
|
||||
missing registry/namespace input (`ADDON_OCI_REPOSITORY`). There is no default registry.
|
||||
The add-on repository namespace is `quay.io/go-skynet/local-ai-addons`; every other
|
||||
missing repository input (`ADDON_OCI_REPOSITORY`). There is no default registry.
|
||||
The add-on repository is `quay.io/go-skynet/local-ai-addons`; every other
|
||||
backend image repository is refused.
|
||||
|
||||
Every platform matrix lists four platforms: `linux/amd64` on `ubuntu-24.04`, `linux/arm64`
|
||||
@@ -3235,15 +3235,22 @@ Each package also has a catalog template in `gallery/addons/<id>.yaml.tmpl`, so
|
||||
listed in Find add-ons once a maintainer renders and publishes its entries. See
|
||||
`gallery/addons/README.md` for the commands.
|
||||
|
||||
Every add-on has its own repository. Tables publishes to `ADDON_OCI_REPOSITORY`. Each
|
||||
other add-on publishes to `<namespace>/<id>`, where the namespace is
|
||||
`ADDON_OCI_REPOSITORY` without its last path element.
|
||||
All add-ons publish to one OCI repository, the value of `ADDON_OCI_REPOSITORY`. Each
|
||||
add-on is a set of tags of that repository, and every tag starts with the add-on id:
|
||||
`<id>-sha-<short sha>` for a commit, `<id>-<version>` for a release, and
|
||||
`<id>-development` or `<id>-latest` for a channel alias. For example, the Fine-tuning
|
||||
build of one commit is
|
||||
`oci://quay.io/go-skynet/local-ai-addons:fine-tuning-sha-99d1dca2c869`.
|
||||
|
||||
Set `ADDON_OCI_REPOSITORY` to the Tables repository inside the add-on namespace:
|
||||
`quay.io/go-skynet/local-ai-addons/tables`. The other add-ons then publish to
|
||||
`quay.io/go-skynet/local-ai-addons/voice`, `.../face`, `.../fine-tuning` and
|
||||
`.../quantization`. The value has no tag and no digest. A value outside the add-on
|
||||
namespace makes the add-on tools refuse the repository.
|
||||
Set `ADDON_OCI_REPOSITORY` to `quay.io/go-skynet/local-ai-addons`. The value has no tag
|
||||
and no digest. A value that is a backend image repository makes the add-on tools refuse
|
||||
it. `addonpkg target --repository <repository> --addon <id> --tag <tag>` prints the
|
||||
reference one add-on publishes to, without a network request.
|
||||
|
||||
The index entries pin each platform image by digest, so the tag only names the
|
||||
artifact. The host accepts a tag reference (`oci://<repository>:<tag>`) and reads the
|
||||
repository with any number of path elements, so an index that still names the old
|
||||
repository-per-add-on form (`oci://<repository>/<id>:<tag>`) keeps working.
|
||||
|
||||
Publishing events (a push to `master`, a version tag and a published release) fail closed
|
||||
for an add-on until at least one of its platforms is enabled in its `platforms.yaml`.
|
||||
@@ -3265,6 +3272,9 @@ place that decides what an event may publish. Its output is what the workflow us
|
||||
| Published, stable, non-draft release | `<semver>` | `latest`, after complete publication |
|
||||
| Published pre-release | `<semver>` | none |
|
||||
|
||||
The names in this table are the part after the add-on id. The registry tag is `<id>-<name>`,
|
||||
for example `tables-sha-4edf30cda051` or `voice-development`.
|
||||
|
||||
Names are in disjoint namespaces: `latest` and `development` are reserved, `<semver>`
|
||||
and `sha-<12 hex>` are immutable, and every branch channel starts with `branch-`. The
|
||||
branch channel is `branch-` + a label + `-` + the first 12 hex characters of the SHA-256
|
||||
|
||||
@@ -14,7 +14,7 @@ go run ./scripts/addons/cmd/addonpkg catalog \
|
||||
--template gallery/addons/tables.yaml.tmpl \
|
||||
--merged <merged layout> \
|
||||
--repository <registry>/<namespace>/<repository> \
|
||||
--tag <immutable tag: the version or sha-<12 hex>> \
|
||||
--tag <immutable tag without the id prefix: the version or sha-<12 hex>> \
|
||||
--channel <latest | development | branch-...> \
|
||||
--owner-repo <owner>/<repository> \
|
||||
--signer-ref <refs/tags/vX.Y.Z | refs/heads/master> \
|
||||
@@ -32,13 +32,14 @@ go run ./scripts/addons/cmd/addonpkg catalog \
|
||||
```
|
||||
|
||||
Use `face`, `fine-tuning`, `quantization`, `studio` or `talk` in place of `voice` for the other packages.
|
||||
Each package has its own repository, so pass the repository of that package.
|
||||
All packages share one repository, so pass the same repository for each. The artifact of an
|
||||
entry is `oci://<repository>:<id>-<tag>`, for example `...:voice-sha-50f0f3de26d2`.
|
||||
|
||||
The add-on packages live in `quay.io/go-skynet/local-ai-addons`; the template itself holds
|
||||
no registry value. The result is a YAML list with one entry for each enabled platform. Each entry:
|
||||
|
||||
- pins the digest of the platform image, so a tag that moves later changes nothing;
|
||||
- names `oci://<repository>:<tag>` for the immutable tag;
|
||||
- names `oci://<repository>:<id>-<tag>` for the immutable tag of that add-on;
|
||||
- carries the channel it is current for and the version of the package, so the
|
||||
channel is metadata that maps to a digest;
|
||||
- states the signature policy: signature required, issuer
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
description: "Train an adapter on your own data, then export it as a model you can chat with."
|
||||
version: "0.0.0-dev.99d1dca2c869"
|
||||
channel: "development"
|
||||
artifact: "oci://quay.io/go-skynet/local-ai-addons/fine-tuning:sha-99d1dca2c869"
|
||||
artifact: "oci://quay.io/go-skynet/local-ai-addons:fine-tuning-sha-99d1dca2c869"
|
||||
digest: "sha256:9e2168ea73c32427e9f7d3c071d2dd36ca37877a78dd3bdbacebd8b9fe6e270a"
|
||||
os: "linux"
|
||||
arch: "amd64"
|
||||
@@ -26,7 +26,7 @@
|
||||
description: "Shrink a model to a smaller quantized file, then import it so you can chat with it."
|
||||
version: "0.0.0-dev.99d1dca2c869"
|
||||
channel: "development"
|
||||
artifact: "oci://quay.io/go-skynet/local-ai-addons/quantization:sha-99d1dca2c869"
|
||||
artifact: "oci://quay.io/go-skynet/local-ai-addons:quantization-sha-99d1dca2c869"
|
||||
digest: "sha256:d1dd5b3cfc7e96cac2825cd8aaf86ebacb49d686f433f1969ad149928d0858ad"
|
||||
os: "linux"
|
||||
arch: "amd64"
|
||||
@@ -44,7 +44,7 @@
|
||||
description: "Recognise who is speaking, keep the voices that text-to-speech can use, and speak text with them."
|
||||
version: "0.0.0-dev.50f0f3de26d2"
|
||||
channel: "development"
|
||||
artifact: "oci://quay.io/go-skynet/local-ai-addons/voice:sha-50f0f3de26d2"
|
||||
artifact: "oci://quay.io/go-skynet/local-ai-addons:voice-sha-50f0f3de26d2"
|
||||
digest: "sha256:1343e18fe7b05e6d65da57f6f86ebf75f2861f24ab029a9179935c98fb613583"
|
||||
os: "linux"
|
||||
arch: "amd64"
|
||||
@@ -62,7 +62,7 @@
|
||||
description: "Find out whose face is in a photo, or whether two photos show the same person."
|
||||
version: "0.0.0-dev.99d1dca2c869"
|
||||
channel: "development"
|
||||
artifact: "oci://quay.io/go-skynet/local-ai-addons/face:sha-99d1dca2c869"
|
||||
artifact: "oci://quay.io/go-skynet/local-ai-addons:face-sha-99d1dca2c869"
|
||||
digest: "sha256:a48ad28afc98b10f952e330243487a0fd3ab2aa4187478e8768712f6b77e86f6"
|
||||
os: "linux"
|
||||
arch: "amd64"
|
||||
@@ -80,7 +80,7 @@
|
||||
description: "Make images, video, 3D models, speech, sound and transformed audio with the models you have installed, and keep what you make."
|
||||
version: "0.0.0-dev.422af83405a3"
|
||||
channel: "development"
|
||||
artifact: "oci://quay.io/go-skynet/local-ai-addons/studio:sha-422af83405a3"
|
||||
artifact: "oci://quay.io/go-skynet/local-ai-addons:studio-sha-422af83405a3"
|
||||
digest: "sha256:1b06153044ba49ce235a56b0739376b75079f38b939ee62c9f3f6f308ddb8073"
|
||||
os: "linux"
|
||||
arch: "amd64"
|
||||
@@ -98,7 +98,7 @@
|
||||
description: "Have a spoken conversation with a model: your voice goes in, the answer is spoken back."
|
||||
version: "0.0.0-dev.422af83405a3"
|
||||
channel: "development"
|
||||
artifact: "oci://quay.io/go-skynet/local-ai-addons/talk:sha-422af83405a3"
|
||||
artifact: "oci://quay.io/go-skynet/local-ai-addons:talk-sha-422af83405a3"
|
||||
digest: "sha256:678569227df72b597f40859a3830bb3a8a17ec02a19050ad71c416b91f4e7951"
|
||||
os: "linux"
|
||||
arch: "amd64"
|
||||
@@ -116,7 +116,7 @@
|
||||
description: "Databases of tables you own or share: typed rows, filters, schema and sharing."
|
||||
version: "0.0.0-dev.4edf30cda051"
|
||||
channel: "development"
|
||||
artifact: "oci://quay.io/go-skynet/local-ai-addons/tables:sha-4edf30cda051"
|
||||
artifact: "oci://quay.io/go-skynet/local-ai-addons:tables-sha-4edf30cda051"
|
||||
digest: "sha256:97863f9c6e848e50cb11617aa1e02d69716e7003d11585388ab5e2547e39d795"
|
||||
os: "linux"
|
||||
arch: "amd64"
|
||||
|
||||
@@ -73,7 +73,7 @@ func cmdCatalog(args []string) error {
|
||||
dir := fs.String("merged", "", "merged layout")
|
||||
config := fs.String("config", "", "platform matrix file")
|
||||
repository := fs.String("repository", "", "registry and repository the package was published to")
|
||||
tag := fs.String("tag", "", "immutable tag the artifact reference names: the version or sha-<commit>")
|
||||
tag := fs.String("tag", "", "immutable tag the artifact reference names, without the add-on id prefix: the version or sha-<commit>")
|
||||
channel := fs.String("channel", "", "channel name the entries are current for: latest, development or a branch channel")
|
||||
ownerRepo := fs.String("owner-repo", "", "GitHub owner/repository that ran the workflow")
|
||||
signerRef := fs.String("signer-ref", "", "ref of the signing workflow run")
|
||||
@@ -152,7 +152,7 @@ func cmdCatalog(args []string) error {
|
||||
}
|
||||
}
|
||||
values := map[string]string{
|
||||
"addon": v.Manifest.ID, "version": m.Version, "tag": *tag, "channel": *channel,
|
||||
"addon": v.Manifest.ID, "version": m.Version, "tag": addonTagOrEmpty(v.Manifest.ID, *tag), "channel": *channel,
|
||||
"repository": repo.Name(), "digest": d.Digest.String(),
|
||||
"os": d.Platform.OS, "arch": d.Platform.Architecture,
|
||||
"issuer": OIDCIssuer, "identity": identity,
|
||||
@@ -171,7 +171,11 @@ func cmdCatalog(args []string) error {
|
||||
return fmt.Errorf("rendered entry: %v", err)
|
||||
}
|
||||
r := parsed[0]
|
||||
if err := checkRelease(r, m.Version, d.Digest.String(), repo.Name(), *tag); err != nil {
|
||||
regTag, err := addonTag(v.Manifest.ID, *tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkRelease(r, m.Version, d.Digest.String(), repo.Name(), regTag); err != nil {
|
||||
return err
|
||||
}
|
||||
releases = append(releases, r)
|
||||
@@ -241,3 +245,13 @@ func checkRelease(r catalogRelease, version, digest, repository, tag string) err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// addonTagOrEmpty is addonTag for the template values. An invalid id yields an
|
||||
// empty tag, which checkRelease then refuses.
|
||||
func addonTagOrEmpty(id, tag string) string {
|
||||
t, err := addonTag(id, tag)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return t
|
||||
}
|
||||
@@ -565,7 +565,7 @@ var _ = Describe("publication", func() {
|
||||
Expect(out).To(Equal(first))
|
||||
_ = before
|
||||
repo, _ := name.NewRepository(reg.repo("addons/tables"))
|
||||
d, err := remote.Head(repo.Tag("1.2.3"))
|
||||
d, err := remote.Head(repo.Tag("tables-1.2.3"))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(first).To(ContainSubstring(d.Digest.String()))
|
||||
children, err := childDigests(ctx, repo, d.Digest)
|
||||
@@ -583,7 +583,7 @@ var _ = Describe("publication", func() {
|
||||
_, err = run("publish", "--config", p.config, "--layout", b, "--repository", repoName, "--tag", "1.2.3")
|
||||
expectCode(err, "E_IMMUTABLE")
|
||||
repo, _ := name.NewRepository(repoName)
|
||||
d, err := remote.Head(repo.Tag("1.2.3"))
|
||||
d, err := remote.Head(repo.Tag("tables-1.2.3"))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
_, da, _ := verifyMerged(a, mustCfg(p))
|
||||
Expect(d.Digest).To(Equal(da))
|
||||
@@ -652,7 +652,7 @@ var _ = Describe("alias promotion", func() {
|
||||
}
|
||||
head := func(tag string) v1.Hash {
|
||||
GinkgoHelper()
|
||||
d, err := remote.Head(repo.Tag(tag))
|
||||
d, err := remote.Head(repo.Tag("tables-" + tag))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
return d.Digest
|
||||
}
|
||||
@@ -669,19 +669,19 @@ var _ = Describe("alias promotion", func() {
|
||||
old := publish("0.0.0-dev."+shas[0][:12], shas[0], "c0", "sha-"+shas[0][:12])
|
||||
mid := publish("0.0.0-dev."+shas[1][:12], shas[1], "c1", "sha-"+shas[1][:12])
|
||||
newer := publish("0.0.0-dev."+shas[2][:12], shas[2], "c2", "sha-"+shas[2][:12])
|
||||
res, err := promoteAlias(ctx, repo, "development", mid, "", shas[1], gitDir)
|
||||
res, err := promoteAlias(ctx, repo, "tables", "development", mid, "", shas[1], gitDir)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res).To(Equal("promoted"))
|
||||
Expect(head("development")).To(Equal(mid))
|
||||
res, err = promoteAlias(ctx, repo, "development", newer, "", shas[2], gitDir)
|
||||
res, err = promoteAlias(ctx, repo, "tables", "development", newer, "", shas[2], gitDir)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res).To(Equal("promoted"))
|
||||
// A slower run of the oldest commit finishes last.
|
||||
res, err = promoteAlias(ctx, repo, "development", old, "", shas[0], gitDir)
|
||||
res, err = promoteAlias(ctx, repo, "tables", "development", old, "", shas[0], gitDir)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res).To(Equal("stale"))
|
||||
Expect(head("development")).To(Equal(newer))
|
||||
res, err = promoteAlias(ctx, repo, "development", newer, "", shas[2], gitDir)
|
||||
res, err = promoteAlias(ctx, repo, "tables", "development", newer, "", shas[2], gitDir)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res).To(Equal("unchanged"))
|
||||
})
|
||||
@@ -699,15 +699,15 @@ var _ = Describe("alias promotion", func() {
|
||||
git("commit", "-q", "--allow-empty", "-m", "rewritten")
|
||||
diverged := git("rev-parse", "HEAD")
|
||||
a := publish("0.0.0-dev."+shas[1][:12], shas[1], "x", "sha-"+shas[1][:12])
|
||||
res, err := promoteAlias(ctx, repo, "development", a, "", shas[1], gitDir)
|
||||
res, err := promoteAlias(ctx, repo, "tables", "development", a, "", shas[1], gitDir)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res).To(Equal("promoted"))
|
||||
d := publish("0.0.0-dev."+diverged[:12], diverged, "z", "sha-"+diverged[:12])
|
||||
res, err = promoteAlias(ctx, repo, "development", d, "", diverged, gitDir)
|
||||
res, err = promoteAlias(ctx, repo, "tables", "development", d, "", diverged, gitDir)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res).To(Equal("promoted"))
|
||||
same := publish("0.0.1", diverged, "y", "0.0.1")
|
||||
_, err = promoteAlias(ctx, repo, "development", same, "", diverged, gitDir)
|
||||
_, err = promoteAlias(ctx, repo, "tables", "development", same, "", diverged, gitDir)
|
||||
expectCode(err, "E_IMMUTABLE")
|
||||
Expect(head("development")).To(Equal(d))
|
||||
})
|
||||
@@ -723,10 +723,10 @@ var _ = Describe("alias promotion", func() {
|
||||
ref := "refs/heads/feature/x"
|
||||
tag := branchTag(ref)
|
||||
oldD := publish("0.0.0-dev."+gone[:12], gone, "old", "sha-"+gone[:12])
|
||||
_, err := promoteAlias(ctx, repo, tag, oldD, "", gone, gitDir)
|
||||
_, err := promoteAlias(ctx, repo, "tables", tag, oldD, "", gone, gitDir)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
newD := publish("0.0.0-dev."+current[:12], current, "new", "sha-"+current[:12])
|
||||
res, err := promoteAliasFrom(ctx, repo, tag, newD, "", current, gitDir, promotionSource{Ref: ref, Event: "push"})
|
||||
res, err := promoteAliasFrom(ctx, repo, "tables", tag, newD, "", current, gitDir, promotionSource{Ref: ref, Event: "push"})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res).To(Equal("promoted"))
|
||||
Expect(head(tag)).To(Equal(newD))
|
||||
@@ -735,7 +735,7 @@ var _ = Describe("alias promotion", func() {
|
||||
ref := "refs/heads/feature/x"
|
||||
tag := branchTag(ref)
|
||||
oldD := publish("0.0.0-dev."+gone[:12], gone, "old", "sha-"+gone[:12])
|
||||
_, err := promoteAlias(ctx, repo, tag, oldD, "", gone, gitDir)
|
||||
_, err := promoteAlias(ctx, repo, "tables", tag, oldD, "", gone, gitDir)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
newD := publish("0.0.0-dev."+current[:12], current, "new", "sha-"+current[:12])
|
||||
for _, src := range []promotionSource{
|
||||
@@ -745,17 +745,17 @@ var _ = Describe("alias promotion", func() {
|
||||
{Ref: "refs/tags/v1.0.0", Event: "push"},
|
||||
{Ref: ref, Event: ""},
|
||||
} {
|
||||
_, err := promoteAliasFrom(ctx, repo, tag, newD, "", current, gitDir, src)
|
||||
_, err := promoteAliasFrom(ctx, repo, "tables", tag, newD, "", current, gitDir, src)
|
||||
expectCode(err, "E_UNREACHABLE")
|
||||
Expect(head(tag)).To(Equal(oldD))
|
||||
}
|
||||
})
|
||||
It("refuses development when its commit is unreachable, even for a push", func() {
|
||||
oldD := publish("0.0.0-dev."+gone[:12], gone, "old", "sha-"+gone[:12])
|
||||
_, err := promoteAlias(ctx, repo, "development", oldD, "", gone, gitDir)
|
||||
_, err := promoteAlias(ctx, repo, "tables", "development", oldD, "", gone, gitDir)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
newD := publish("0.0.0-dev."+current[:12], current, "new", "sha-"+current[:12])
|
||||
_, err = promoteAliasFrom(ctx, repo, "development", newD, "", current, gitDir, promotionSource{Ref: "refs/heads/master", Event: "push"})
|
||||
_, err = promoteAliasFrom(ctx, repo, "tables", "development", newD, "", current, gitDir, promotionSource{Ref: "refs/heads/master", Event: "push"})
|
||||
expectCode(err, "E_UNREACHABLE")
|
||||
Expect(head("development")).To(Equal(oldD))
|
||||
})
|
||||
@@ -788,43 +788,43 @@ var _ = Describe("alias promotion", func() {
|
||||
It("moves latest only to a newer stable version", func() {
|
||||
v1d := publish("1.0.0", rev, "one", "1.0.0")
|
||||
v2d := publish("1.1.0", rev, "two", "1.1.0")
|
||||
res, err := promoteAlias(ctx, repo, "latest", v2d, "1.1.0", rev, "")
|
||||
res, err := promoteAlias(ctx, repo, "tables", "latest", v2d, "1.1.0", rev, "")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res).To(Equal("promoted"))
|
||||
res, err = promoteAlias(ctx, repo, "latest", v1d, "1.0.0", rev, "")
|
||||
res, err = promoteAlias(ctx, repo, "tables", "latest", v1d, "1.0.0", rev, "")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res).To(Equal("stale"))
|
||||
Expect(head("latest")).To(Equal(v2d))
|
||||
_, err = promoteAlias(ctx, repo, "latest", v2d, "1.2.0-rc.1", rev, "")
|
||||
_, err = promoteAlias(ctx, repo, "tables", "latest", v2d, "1.2.0-rc.1", rev, "")
|
||||
expectCode(err, "E_RESERVED")
|
||||
other := publish("1.1.0", "fedcba9876543210fedcba9876543210fedcba98", "three", "sha-fedcba987654")
|
||||
_, err = promoteAlias(ctx, repo, "latest", other, "1.1.0", rev, "")
|
||||
_, err = promoteAlias(ctx, repo, "tables", "latest", other, "1.1.0", rev, "")
|
||||
expectCode(err, "E_IMMUTABLE")
|
||||
Expect(head("latest")).To(Equal(v2d))
|
||||
})
|
||||
It("refuses reserved or foreign alias names and unpublished digests, leaving aliases unchanged", func() {
|
||||
d := publish("1.0.0", rev, "one", "1.0.0")
|
||||
for _, alias := range []string{"1.0.0", "sha-0123456789ab", "main", "branch-", "Latest", ""} {
|
||||
_, err := promoteAlias(ctx, repo, alias, d, "1.0.0", rev, "")
|
||||
_, err := promoteAlias(ctx, repo, "tables", alias, d, "1.0.0", rev, "")
|
||||
expectCode(err, "E_RESERVED")
|
||||
}
|
||||
missing, _ := v1.NewHash("sha256:" + strings.Repeat("a", 64))
|
||||
before := reg.writeCount()
|
||||
_, err := promoteAlias(ctx, repo, "latest", missing, "1.0.0", rev, "")
|
||||
_, err := promoteAlias(ctx, repo, "tables", "latest", missing, "1.0.0", rev, "")
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(reg.writeCount()).To(Equal(before))
|
||||
_, err = remote.Head(repo.Tag("latest"))
|
||||
_, err = remote.Head(repo.Tag("tables-latest"))
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
It("promotes a branch channel under its hashed name", func() {
|
||||
gitDir, shas := gitHistory(1)
|
||||
tag := branchTag("refs/heads/feature/x")
|
||||
d := publish("0.0.0-dev."+shas[0][:12], shas[0], "b", "sha-"+shas[0][:12])
|
||||
res, err := promoteAlias(ctx, repo, tag, d, "", shas[0], gitDir)
|
||||
res, err := promoteAlias(ctx, repo, "tables", tag, d, "", shas[0], gitDir)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res).To(Equal("promoted"))
|
||||
Expect(head(tag)).To(Equal(d))
|
||||
_, err = promoteAlias(ctx, repo, "branch-feature-x", d, "", shas[0], gitDir)
|
||||
_, err = promoteAlias(ctx, repo, "tables", "branch-feature-x", d, "", shas[0], gitDir)
|
||||
expectCode(err, "E_RESERVED")
|
||||
})
|
||||
})
|
||||
@@ -853,7 +853,7 @@ var _ = Describe("catalog template", func() {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
for _, e := range entries {
|
||||
Expect(e.Digest).To(MatchRegexp(`^sha256:[0-9a-f]{64}$`))
|
||||
Expect(e.Artifact).To(Equal("oci://" + repoName + ":1.2.3"))
|
||||
Expect(e.Artifact).To(Equal("oci://" + repoName + ":tables-1.2.3"))
|
||||
Expect(e.Policy.RequireSignature).To(BeTrue())
|
||||
Expect(e.Policy.Identity).To(Equal("https://github.com/example/localai/.github/workflows/addons-package.yml@refs/tags/v1.2.3"))
|
||||
Expect(e.Policy.Issuer).To(Equal("https://token.actions.githubusercontent.com"))
|
||||
@@ -868,7 +868,7 @@ var _ = Describe("catalog template", func() {
|
||||
for _, id := range []string{"tables", "voice", "face", "fine-tuning", "quantization", "studio", "talk"} {
|
||||
p := newProjectFor(id, both)
|
||||
m := p.merged("1.2.3", rev, nil)
|
||||
repoName := newRegistry().repo("addons/" + id)
|
||||
repoName := newRegistry().repo("addons")
|
||||
out := filepath.Join(GinkgoT().TempDir(), "catalog.yaml")
|
||||
_, err := run("catalog", "--config", p.config, "--template", "../../../gallery/addons/"+id+".yaml.tmpl", "--merged", m, "--repository", repoName, "--tag", "1.2.3", "--channel", "latest", "--owner-repo", "example/localai", "--signer-ref", "refs/tags/v1.2.3", "--out", out)
|
||||
Expect(err).NotTo(HaveOccurred(), id)
|
||||
@@ -879,6 +879,7 @@ var _ = Describe("catalog template", func() {
|
||||
Expect(entries).To(HaveLen(2), id)
|
||||
for _, e := range entries {
|
||||
Expect(e.ID).To(Equal(id))
|
||||
Expect(e.Artifact).To(Equal("oci://" + repoName + ":" + id + "-1.2.3"))
|
||||
Expect(e.Policy.RequireSignature).To(BeTrue())
|
||||
}
|
||||
}
|
||||
@@ -1028,8 +1029,8 @@ var _ = Describe("workflow policy", func() {
|
||||
It("rejects publication that is shared between add-ons", func() {
|
||||
expect(mutate("addons-promote-${{ matrix.addon }}-", "addons-promote-"), "P_CONCURRENCY")
|
||||
expect(mutate("addons-publish-${{ matrix.addon }}-", "addons-publish-"), "P_CONCURRENCY")
|
||||
expect(mutate(" addon: ${{ fromJSON(needs.plan.outputs.addons) }}\n env:\n ADDON_OCI_BASE: ${{ vars.ADDON_OCI_REPOSITORY }}\n ADDON: ${{ matrix.addon }}\n #", " #"), "P_MATRIX")
|
||||
expect(mutate("repo=\"${ADDON_OCI_BASE%/*}/$ADDON\"", "repo=\"$ADDON_OCI_BASE\""), "P_REPOSITORY")
|
||||
expect(mutate(" addon: ${{ fromJSON(needs.plan.outputs.addons) }}\n env:\n ADDON_OCI_REPOSITORY: ${{ vars.ADDON_OCI_REPOSITORY }}\n ADDON: ${{ matrix.addon }}\n #", " #"), "P_MATRIX")
|
||||
expect(mutate(" ADDON: ${{ matrix.addon }}\n # cosign", " ADDON: ${{ matrix.addon }}\n ADDON_OCI_BASE: x\n # cosign"), "P_REPOSITORY")
|
||||
expect(mutate(" COSIGN_EXPERIMENTAL: '1'\n", " COSIGN_EXPERIMENTAL: '1'\n outputs:\n digest: x\n"), "P_ORDER")
|
||||
})
|
||||
It("rejects a workflow that names one add-on by path or skips an add-on in the matrix", func() {
|
||||
@@ -1047,13 +1048,13 @@ var _ = Describe("workflow policy", func() {
|
||||
expect(mutate(" elif grep -q 'no enabled platform' \"$err\"; then", " elif true; then"), "P_FAIL_CLOSED")
|
||||
expect(mutate(" echo \"cannot read the platform matrix of add-on $id\" >&2\n cat \"$err\" >&2\n exit 1", " continue"), "P_FAIL_CLOSED")
|
||||
})
|
||||
It("rejects a repository rule that does not keep the configured repository for tables", func() {
|
||||
expect(mutate(" if [ \"$ADDON\" = tables ]; then\n repo=\"$ADDON_OCI_BASE\"", " if [ \"$ADDON\" = never ]; then\n repo=\"$ADDON_OCI_BASE\""), "P_REPOSITORY")
|
||||
expect(mutate(" repo=\"$ADDON_OCI_BASE\"\n else", " repo=\"${ADDON_OCI_BASE%/*}/$ADDON\"\n else"), "P_REPOSITORY")
|
||||
It("rejects a repository rule that derives a repository for each add-on", func() {
|
||||
expect(mutate(" env:\n ADDON_OCI_REPOSITORY: ${{ vars.ADDON_OCI_REPOSITORY }}\n ADDON: ${{ matrix.addon }}\n # cosign", " env:\n ADDON_OCI_BASE: ${{ vars.ADDON_OCI_REPOSITORY }}\n ADDON: ${{ matrix.addon }}\n # cosign"), "P_REPOSITORY")
|
||||
expect(mutate("ADDON_OCI_REPOSITORY: ${{ vars.ADDON_OCI_REPOSITORY }}\n ADDON: ${{ matrix.addon }}\n steps:\n - uses: actions/checkout@v7\n with:\n fetch-depth: 0", "ADDON_OCI_REPOSITORY: quay.io/example/x\n ADDON: ${{ matrix.addon }}\n steps:\n - uses: actions/checkout@v7\n with:\n fetch-depth: 0"), "P_REPOSITORY")
|
||||
})
|
||||
It("rejects a publish or promote matrix that stops the other add-ons on a failure", func() {
|
||||
expect(mutate(" # A leg that is pushing must finish, so one failure never cancels another.\n fail-fast: false", " fail-fast: true"), "P_FAIL_FAST")
|
||||
expect(mutate(" permissions:\n contents: read\n strategy:\n fail-fast: false\n matrix:\n addon: ${{ fromJSON(needs.plan.outputs.addons) }}\n env:\n ADDON_OCI_BASE: ${{ vars.ADDON_OCI_REPOSITORY }}\n ADDON: ${{ matrix.addon }}\n steps:", " permissions:\n contents: read\n strategy:\n fail-fast: true\n matrix:\n addon: ${{ fromJSON(needs.plan.outputs.addons) }}\n env:\n ADDON_OCI_BASE: ${{ vars.ADDON_OCI_REPOSITORY }}\n ADDON: ${{ matrix.addon }}\n steps:"), "P_FAIL_FAST")
|
||||
expect(mutate(" permissions:\n contents: read\n strategy:\n fail-fast: false\n matrix:\n addon: ${{ fromJSON(needs.plan.outputs.addons) }}\n env:\n ADDON_OCI_REPOSITORY: ${{ vars.ADDON_OCI_REPOSITORY }}\n ADDON: ${{ matrix.addon }}\n steps:", " permissions:\n contents: read\n strategy:\n fail-fast: true\n matrix:\n addon: ${{ fromJSON(needs.plan.outputs.addons) }}\n env:\n ADDON_OCI_REPOSITORY: ${{ vars.ADDON_OCI_REPOSITORY }}\n ADDON: ${{ matrix.addon }}\n steps:"), "P_FAIL_FAST")
|
||||
})
|
||||
It("rejects a publish job that downloads the layouts of other add-ons", func() {
|
||||
expect(mutate("pattern: layout-${{ matrix.addon }}-*", "pattern: layout-*"), "P_ARTIFACTS")
|
||||
@@ -1165,8 +1166,8 @@ var _ = Describe("the add-on package workflow", func() {
|
||||
})
|
||||
|
||||
// documentedAddonRepository is the value the documentation gives for the
|
||||
// ADDON_OCI_REPOSITORY variable: the Tables repository in the add-on namespace.
|
||||
const documentedAddonRepository = "quay.io/go-skynet/local-ai-addons/tables"
|
||||
// ADDON_OCI_REPOSITORY variable: the one repository of all add-ons.
|
||||
const documentedAddonRepository = "quay.io/go-skynet/local-ai-addons"
|
||||
|
||||
// workflowStepScript returns the shell script and env of a named step of a job.
|
||||
func workflowStepScript(workflow, job, step string) (script string) {
|
||||
@@ -1198,31 +1199,24 @@ var _ = Describe("workflow behavior", func() {
|
||||
workflow = string(b)
|
||||
})
|
||||
|
||||
It("derives a repository that the add-on tools accept for every add-on from the documented variable", func() {
|
||||
It("publishes every add-on to the one documented repository, with its id as the tag prefix", func() {
|
||||
docs, err := os.ReadFile("../../../docs/content/features/addons.md")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(string(docs)).To(ContainSubstring("`" + documentedAddonRepository + "`"))
|
||||
for _, job := range []string{"publish", "promote"} {
|
||||
script := workflowStepScript(workflow, job, "Choose the repository of this add-on")
|
||||
for _, id := range firstPartyAddons {
|
||||
env := filepath.Join(GinkgoT().TempDir(), "github-env")
|
||||
Expect(os.WriteFile(env, nil, 0o644)).To(Succeed())
|
||||
cmd := exec.Command("sh", "-c", script)
|
||||
cmd.Env = append(os.Environ(), "ADDON_OCI_BASE="+documentedAddonRepository, "ADDON="+id, "GITHUB_ENV="+env)
|
||||
out, err := cmd.CombinedOutput()
|
||||
Expect(err).NotTo(HaveOccurred(), string(out))
|
||||
written, err := os.ReadFile(env)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
repo := strings.TrimSpace(strings.TrimPrefix(string(written), "ADDON_OCI_REPOSITORY="))
|
||||
if id == "tables" {
|
||||
Expect(repo).To(Equal(documentedAddonRepository), "%s keeps the configured repository", job)
|
||||
} else {
|
||||
Expect(repo).To(Equal("quay.io/go-skynet/local-ai-addons/"+id), job)
|
||||
}
|
||||
_, err = parseRepository(repo)
|
||||
Expect(err).NotTo(HaveOccurred(), "%s: %s for %s", job, repo, id)
|
||||
}
|
||||
Expect(workflow).NotTo(ContainSubstring("ADDON_OCI_BASE"))
|
||||
Expect(workflow).NotTo(ContainSubstring("Choose the repository of this add-on"))
|
||||
_ = job
|
||||
}
|
||||
for _, id := range firstPartyAddons {
|
||||
out, err := run("target", "--repository", documentedAddonRepository, "--addon", id, "--tag", "sha-99d1dca2c869")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(strings.TrimSpace(out)).To(Equal("target=" + documentedAddonRepository + ":" + id + "-sha-99d1dca2c869"))
|
||||
}
|
||||
_, err = run("target", "--repository", documentedAddonRepository, "--addon", "Bad_ID", "--tag", "sha-99d1dca2c869")
|
||||
Expect(err).To(HaveOccurred())
|
||||
_, err = run("target", "--repository", "quay.io/go-skynet/local-ai-backends", "--addon", "tables", "--tag", "sha-99d1dca2c869")
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
Describe("the plan step", func() {
|
||||
|
||||
@@ -229,23 +229,21 @@ func checkWorkflow(data []byte) []string {
|
||||
add("P_MATRIX", "%s must run once for each add-on: strategy.matrix.addon is missing", n)
|
||||
}
|
||||
}
|
||||
// Publication is per add-on: the groups, the repository and the digest
|
||||
// must not be shared between add-ons.
|
||||
// Publication is per add-on: the groups and the digest must not be shared
|
||||
// between add-ons. The repository is shared; the tag carries the id.
|
||||
for _, n := range []string{"publish", "promote"} {
|
||||
j := job(n)
|
||||
c, _ := j["concurrency"].(map[string]any)
|
||||
if !strings.Contains(fmt.Sprint(c["group"]), "matrix.addon") {
|
||||
add("P_CONCURRENCY", "%s must serialize per add-on: the concurrency group needs matrix.addon", n)
|
||||
}
|
||||
if t := text(j); !strings.Contains(t, `ADDON_OCI_REPOSITORY=$repo" >> "$GITHUB_ENV"`) || !strings.Contains(t, `${ADDON_OCI_BASE%/*}/$ADDON`) {
|
||||
add("P_REPOSITORY", "%s must choose the repository of the add-on: <namespace>/<id>, and the configured repository for tables", n)
|
||||
if t := text(j); strings.Contains(t, "ADDON_OCI_BASE") || strings.Contains(t, "ADDON_OCI_REPOSITORY%/*") || strings.Contains(t, "ADDON_OCI_REPOSITORY=$repo") {
|
||||
add("P_REPOSITORY", "%s must use the configured repository as it is: every add-on is a tag of one repository", n)
|
||||
}
|
||||
}
|
||||
// Tables keeps the configured repository and every other add-on derives its
|
||||
// own from the namespace.
|
||||
for _, n := range []string{"publish", "promote"} {
|
||||
if !regexp.MustCompile(`if \[ "\$ADDON" = tables \]; then\s+repo="\$ADDON_OCI_BASE"\s+else\s+repo="\$\{ADDON_OCI_BASE%/\*\}/\$ADDON"\s+fi`).MatchString(text(job(n))) {
|
||||
add("P_REPOSITORY", "%s must keep ADDON_OCI_REPOSITORY for tables and derive <namespace>/<id> for every other add-on", n)
|
||||
if !strings.Contains(text(job(n)), "ADDON_OCI_REPOSITORY: ${{ vars.ADDON_OCI_REPOSITORY }}") {
|
||||
add("P_REPOSITORY", "%s must read ADDON_OCI_REPOSITORY from the repository variable", n)
|
||||
}
|
||||
// A leg that is pushing or moving an alias must finish, and one
|
||||
// failing add-on must not stop another.
|
||||
|
||||
@@ -24,8 +24,20 @@ import (
|
||||
var (
|
||||
immutableTagRE = regexp.MustCompile(`^(sha-[0-9a-f]{12}|(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z]+(\.[0-9A-Za-z-]+)*)?)$`)
|
||||
branchAliasRE = regexp.MustCompile(`^branch-[a-z0-9]+(-[a-z0-9]+)*-[0-9a-f]{12}$`)
|
||||
addonIDRE = regexp.MustCompile(`^[a-z][a-z0-9]*(-[a-z0-9]+)*$`)
|
||||
)
|
||||
|
||||
// addonTag is the registry tag of one add-on in the shared add-on repository:
|
||||
// the add-on id, a dash, then the immutable tag or the channel alias. Every
|
||||
// add-on lives in the same repository, so the id prefix keeps their tags apart,
|
||||
// for example fine-tuning-sha-99d1dca2c869 or tables-development.
|
||||
func addonTag(id, tag string) (string, error) {
|
||||
if !addonIDRE.MatchString(id) {
|
||||
return "", fmt.Errorf("%q is not an add-on id", id)
|
||||
}
|
||||
return id + "-" + tag, nil
|
||||
}
|
||||
|
||||
// backendRepositoryPrefixes are repositories that belong to the backend
|
||||
// image pipeline. Add-on packages never go there, whatever the input says.
|
||||
var backendRepositoryPrefixes = []string{
|
||||
@@ -110,7 +122,11 @@ func publishIndex(ctx context.Context, dir string, cfg *PlatformsFile, repo name
|
||||
} else if immutable != m.Version {
|
||||
return zero, fail("E_RESERVED", "version tag %q differs from the layout version %q", immutable, m.Version)
|
||||
}
|
||||
tag := repo.Tag(immutable)
|
||||
regTag, err := addonTag(cfg.Addon, immutable)
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
tag := repo.Tag(regTag)
|
||||
if _, err := guardImmutable(ctx, tag, digest); err != nil {
|
||||
if adopt && strings.HasPrefix(err.Error(), "E_IMMUTABLE") {
|
||||
// A rebuild of the same version and commit differs in bytes only
|
||||
@@ -195,7 +211,7 @@ func adoptExisting(ctx context.Context, repo name.Repository, tag name.Tag, m *m
|
||||
func cmdPublish(args []string) error {
|
||||
fs := flag.NewFlagSet("publish", flag.ContinueOnError)
|
||||
dir := fs.String("layout", "", "merged layout")
|
||||
repository := fs.String("repository", "", "registry and repository, for example registry.example/owner/addon-tables")
|
||||
repository := fs.String("repository", "", "shared add-on registry and repository; every add-on is a tag of it, prefixed with its id")
|
||||
immutable := fs.String("tag", "", "immutable version or commit tag")
|
||||
config := fs.String("config", "", "platform matrix file")
|
||||
adopt := fs.Bool("adopt-existing", false, "keep a published index of the same version and commit when a rebuild differs")
|
||||
@@ -239,8 +255,12 @@ type aliasState struct {
|
||||
revision string
|
||||
}
|
||||
|
||||
func readAlias(ctx context.Context, repo name.Repository, alias string) (*aliasState, error) {
|
||||
ref := repo.Tag(alias)
|
||||
func readAlias(ctx context.Context, repo name.Repository, addon, alias string) (*aliasState, error) {
|
||||
regTag, err := addonTag(addon, alias)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ref := repo.Tag(regTag)
|
||||
d, err := remote.Get(ref, remoteOptions(ctx)...)
|
||||
if err != nil {
|
||||
if isNotFound(err) {
|
||||
@@ -391,14 +411,14 @@ func (s promotionSource) pushToOwnBranch(alias string) bool {
|
||||
// promoteAlias moves one moving tag to a digest that was already published,
|
||||
// verified and signed. It never moves an alias backwards: a slower, older run
|
||||
// finds a newer alias and leaves it alone.
|
||||
func promoteAlias(ctx context.Context, repo name.Repository, alias string, digest v1.Hash, version, revision, gitDir string) (string, error) {
|
||||
return promoteAliasFrom(ctx, repo, alias, digest, version, revision, gitDir, promotionSource{})
|
||||
func promoteAlias(ctx context.Context, repo name.Repository, addon, alias string, digest v1.Hash, version, revision, gitDir string) (string, error) {
|
||||
return promoteAliasFrom(ctx, repo, addon, alias, digest, version, revision, gitDir, promotionSource{})
|
||||
}
|
||||
|
||||
// promoteAliasFrom is promoteAlias with the event that asks for the move. A
|
||||
// branch channel whose current commit is gone from the checkout (the branch
|
||||
// was force-pushed) accepts only a push to its own ref as the new truth.
|
||||
func promoteAliasFrom(ctx context.Context, repo name.Repository, alias string, digest v1.Hash, version, revision, gitDir string, src promotionSource) (string, error) {
|
||||
func promoteAliasFrom(ctx context.Context, repo name.Repository, addon, alias string, digest v1.Hash, version, revision, gitDir string, src promotionSource) (string, error) {
|
||||
switch {
|
||||
case alias == aliasLatest:
|
||||
if !versionRE.MatchString(version) || strings.Contains(version, "-") {
|
||||
@@ -411,7 +431,7 @@ func promoteAliasFrom(ctx context.Context, repo name.Repository, alias string, d
|
||||
default:
|
||||
return "", fail("E_RESERVED", "%q is not a channel alias", alias)
|
||||
}
|
||||
cur, err := readAlias(ctx, repo, alias)
|
||||
cur, err := readAlias(ctx, repo, addon, alias)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -462,7 +482,11 @@ func promoteAliasFrom(ctx context.Context, repo name.Repository, alias string, d
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := remote.Tag(repo.Tag(alias), idx, remoteOptions(ctx)...); err != nil {
|
||||
regTag, err := addonTag(addon, alias)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := remote.Tag(repo.Tag(regTag), idx, remoteOptions(ctx)...); err != nil {
|
||||
return "", fmt.Errorf("tagging %s: %w", alias, err)
|
||||
}
|
||||
return promoted, nil
|
||||
@@ -471,6 +495,7 @@ func promoteAliasFrom(ctx context.Context, repo name.Repository, alias string, d
|
||||
func cmdPromote(args []string) error {
|
||||
fs := flag.NewFlagSet("promote", flag.ContinueOnError)
|
||||
repository := fs.String("repository", "", "registry and repository")
|
||||
addon := fs.String("addon", "", "add-on id; the alias tag is <id>-<alias> in the shared repository")
|
||||
alias := fs.String("alias", "", "latest, development or a branch channel tag")
|
||||
digest := fs.String("digest", "", "published index digest")
|
||||
version := fs.String("version", "", "release version of the digest")
|
||||
@@ -497,7 +522,7 @@ func cmdPromote(args []string) error {
|
||||
if _, err := os.Stat(*gitDir); err != nil && *alias != aliasLatest {
|
||||
return fmt.Errorf("git directory: %w", err)
|
||||
}
|
||||
out, err := promoteAliasFrom(ctx, repo, *alias, h, *version, *revision, *gitDir, promotionSource{Ref: *ref, Event: *event})
|
||||
out, err := promoteAliasFrom(ctx, repo, *addon, *alias, h, *version, *revision, *gitDir, promotionSource{Ref: *ref, Event: *event})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -526,3 +551,29 @@ func childDigests(ctx context.Context, repo name.Repository, digest v1.Hash) ([]
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// cmdTarget prints the registry reference one add-on publishes to: the shared
|
||||
// repository and the tag <id>-<tag>. It does not touch the network, so the
|
||||
// workflow and the tests can show the target before anything is pushed.
|
||||
func cmdTarget(args []string) error {
|
||||
fs := flag.NewFlagSet("target", flag.ContinueOnError)
|
||||
repository := fs.String("repository", "", "shared add-on registry and repository")
|
||||
addon := fs.String("addon", "", "add-on id")
|
||||
tag := fs.String("tag", "", "immutable version or commit tag, or a channel alias")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
repo, err := parseRepository(*repository)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *tag == "" {
|
||||
return errors.New("--tag is required")
|
||||
}
|
||||
regTag, err := addonTag(*addon, *tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = fmt.Fprintf(stdout, "target=%s\n", repo.Tag(regTag))
|
||||
return nil
|
||||
}
|
||||
@@ -27,6 +27,7 @@ var commands = map[string]func(args []string) error{
|
||||
"merge": cmdMerge,
|
||||
"publish": cmdPublish,
|
||||
"promote": cmdPromote,
|
||||
"target": cmdTarget,
|
||||
"catalog": cmdCatalog,
|
||||
"policy": cmdPolicy,
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user