Skip to content
43 changes: 15 additions & 28 deletions .agents/skills/merge-upstream/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ description: Merge werf upstream into the deckhouse/delivery-kit fork. Resolves

- **Upstream** = `werf/werf` (fetch/merge source). **Fork** = `deckhouse/delivery-kit` (push target).
- Remote names vary per clone, so Step 0 resolves both by URL into `$UPSTREAM` / `$FORK`.
- `CHANGELOG.md` is release-please-managed (`release-type: go`, runs on push to `main`). Only `-dk`
entries are authored by hand; bare upstream blocks (`## [X.Y.Z]`) already in history stay, but
you never add new ones.
- `CHANGELOG.md` is release-please-managed (`release-type: go`, runs on push to `main`).
Nothing from `upstream/main` ever lands in it: on conflict you always take ours and never copy,
author, or prepend any entry — the changelog is release-please's job on push to `main`, not the
agent's. The agent only pins the release version via an empty `Release-As` commit (Step 2).
- Requires `gh` authenticated and a clean working tree.

The agent merges, resolves conflicts, and opens a PR — it never pushes to the fork's `main`.
Expand Down Expand Up @@ -45,7 +46,8 @@ merge subject identical with or without conflicts.
Resolve conflicts:

- **`CHANGELOG.md`** — always take ours: `git checkout --ours CHANGELOG.md && git add CHANGELOG.md`.
Upstream changelog changes are dropped; the `-dk` entry is authored in Step 2.
Upstream changelog changes are dropped. Do NOT author or prepend any entry — release-please
generates the changelog on push to `main`.
- **`go.mod` / `go.sum`** — resolve obvious parts, then `go mod tidy && git add go.mod go.sum`.
Never blindly take one side.
- **Any other file** — do not blanket-take upstream; it can silently revert delivery-kit
Expand All @@ -58,39 +60,24 @@ Stage resolved tracked files only, then commit:
git add -u && git commit --no-edit
```

### 2. Author the `-dk` changelog entry and force the release version
### 2. Force the release version (no changelog)

Do this after a conflict-free merge exists. Use the Step 1 preview for the commit list (and
`gh pr view <url> --json commits` if a delivery-kit PR URL was given).

Skip release-please noise (`chore(main): release …`, `chore(release): N alpha,beta`).
Do NOT author or prepend anything in `CHANGELOG.md` — release-please generates it on push to
`main`. The only release artifact the agent adds is an empty `Release-As` commit that pins the
exact `-dk` version (release-please would otherwise infer it from commit history).

**Determine the next `-dk` version** from the upstream base being merged: if upstream moved
`2.72.x → 2.73.0`, it is `2.73.0-dk`; if the upstream base is unchanged and you add only fork-side
fixes, bump the `-dk` patch. Never blindly +1 the latest `-dk` patch across an upstream minor/major.

**Prepend the changelog block** below `# Changelog` (today's date), then commit:

```
## [X.Y.Z-dk](https://github.com/deckhouse/delivery-kit/compare/vPREV-dk...vX.Y.Z-dk) (YYYY-MM-DD)
### Features / Bug Fixes — using deckhouse/delivery-kit links, not werf/werf
```

```bash
git add CHANGELOG.md && git commit -m "chore(release): resolve changelog for X.Y.Z-dk"
```

**Force the Release Please version** with an empty commit carrying `Release-As`. This overrides the
SemVer bump Release Please would calculate from commit history and pins the exact `-dk` version:

```bash
git commit --allow-empty -m "chore: force release X.Y.Z-dk

Release-As: vX.Y.Z-dk"
```

`Release-As: vX.Y.Z-dk` must be in the **commit body** (blank line after subject), not the subject
line. The value must include the `v` prefix. Use the same version as the changelog entry above.
line. The value must include the `v` prefix.

### 3. Regenerate docs, build, test

Expand All @@ -108,14 +95,14 @@ If build or tests fail, stop and resolve (or surface for a maintainer) before th

```bash
git grep -q '^<<<<<<<' && { echo "ABORT: conflict markers"; exit 1; } # MUST find none
head -5 CHANGELOG.md # top MUST be the new -dk block
git diff "$FORK/main" -- CHANGELOG.md # MUST be empty (nothing from upstream)
git status # MUST be clean

git push -u "$FORK" chore/release/merge-werf-upstream
gh pr create --repo deckhouse/delivery-kit --base main \
--head chore/release/merge-werf-upstream \
--title "chore(release): merge werf upstream into delivery-kit (X.Y.Z-dk)" \
--body "Sync werf upstream. CHANGELOG.md kept -dk-only. New release entry: X.Y.Z-dk."
--body "Sync werf upstream. CHANGELOG.md unchanged; release-please generates it on merge. Release pinned to X.Y.Z-dk via Release-As."
```

The agent stops after opening the PR; a maintainer reviews and merges.
Expand All @@ -129,7 +116,7 @@ To recover before pushing: `git merge --abort`, or discard the branch with
`git-commit-message`, and `pull-request-name` skills for any other naming.
- ALWAYS work on the `chore/release/...` branch and finish with a PR; NEVER push to the fork's `main`.
- ALWAYS run `task doc:gen`, `task build`, `task test:unit` before the PR; NEVER open it with a broken build or remaining conflict markers.
- ALWAYS add an empty `Release-As: vX.Y.Z-dk` commit (Step 2) so Release Please proposes the correct version; NEVER rely on SemVer bump inference from commit history alone.
- CHANGELOG: NEVER add a bare upstream block or reorder existing entries; only prepend one `-dk` block, and take ours (`--ours`) on conflict.
- CHANGELOG: NEVER copy, author, prepend, or reorder any entry; take ours (`--ours`) on conflict and leave it byte-identical to `$FORK/main`. The changelog is release-please's job.
- ALWAYS add an empty `Release-As: vX.Y.Z-dk` commit (Step 2) so release-please pins the correct version; NEVER author a changelog entry for it — release-please generates the changelog on push to `main`.
- NEVER `git add .`; stage only resolved tracked files.
- NEVER blanket-resolve non-CHANGELOG conflicts toward upstream; stop and ask a human.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,4 @@ buildah-test
/playground/s3_registry/data

/.sisyphus
/.opencode
/.opencode
2 changes: 1 addition & 1 deletion Taskfile.dist.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ vars:
devBinary: './bin/werf{{if (eq .targetOS "windows")}}.exe{{end}}'
withCoverageBinary: './bin/werf-with-coverage{{if (eq .targetOS "windows")}}.exe{{end}}'
package: "github.com/werf/werf/v2/cmd/werf"
kubeVersion: '{{.kubeVersion | default "1.33.1"}}'
kubeVersion: '{{.kubeVersion | default "1.36.1"}}'

goTags: "dfrunsecurity dfrunnetwork dfrunmount dfssh containers_image_openpgp"
goLDFlags: "-s -w -X github.com/werf/werf/v2/pkg/werf.Version={{.version}}"
Expand Down
6 changes: 4 additions & 2 deletions docs/pages_en/usage/cleanup/cr_cleanup.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ The documentation section about ["Saving the result of work"](#generate-keep-lis
By default, werf uses the [_Docker Registry API_](https://docs.docker.com/registry/spec/api/) for deleting tags. The user must be authenticated and have a sufficient set of permissions. If the _Docker Registry API_ isn't supported and tags are deleted using the native API, then some additional container registry-specific actions are required on the user's part.

| | |
|-----------------------------|:---------------------------:|
| --------------------------- | :-------------------------: |
| _AWS ECR_ | [***ok**](#aws-ecr) |
| _Azure CR_ | [***ok**](#azure-cr) |
| _Default_ | **ok** |
Expand Down Expand Up @@ -238,7 +238,9 @@ You can use the `--repo-github-token` option or the corresponding environment va

werf uses the _GitLab container registry API_ or _Docker Registry API_ (depending on the GitLab version) to delete tags.

> The privileges of the temporary CI job token ($CI_JOB_TOKEN) are not sufficient to delete tags. Therefore, the user must create a dedicated token in the Access Token section, select api in the Scope section, and ensure the role of Maintainer or Owner is assigned before using it for authorization
> You can use the temporary CI job token (`$CI_JOB_TOKEN`) to delete tags if the user who started the job has sufficient permissions in the project and the project Job token permissions allow the required access.
>
> If these conditions are not met (for example, because of cross-project restrictions or an insufficient role), use a dedicated Project/Personal Access Token with the `api` scope.

## Saving the result of work

Expand Down
6 changes: 4 additions & 2 deletions docs/pages_ru/usage/cleanup/cr_cleanup.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ my-custom-tag
По умолчанию при удалении тегов werf использует [_Docker Registry API_](https://docs.docker.com/registry/spec/api/) и от пользователя требуется только авторизация с использованием доступов с достаточным набором прав. Если же удаление посредством _Docker Registry API_ не поддерживается и оно реализуется в нативном API container registry, то от пользователя могут потребоваться специфичные для используемого container registry действия.

| | |
|-----------------------------|:---------------------------:|
| --------------------------- | :-------------------------: |
| _AWS ECR_ | [***ок**](#aws-ecr) |
| _Azure CR_ | [***ок**](#azure-cr) |
| _Default_ | **ок** |
Expand Down Expand Up @@ -237,7 +237,9 @@ HUB_TOKEN=$(curl -s -H "Content-Type: application/json" -X POST -d '{"username":

При удалении тегов werf использует _GitLab container registry API_ или _Docker Registry API_ в зависимости от версии GitLab.

> Прав временного токена CI-задания ($CI_JOB_TOKEN) недостаточно для удаления тегов, поэтому пользователю необходимо создать специальный токен в разделе Access Token, выбрать api в секции Scope и назначить роль Maintainer или Owner перед использованием его для авторизации
> Для удаления тегов можно использовать временный токен CI-задания (`$CI_JOB_TOKEN`), если пользователь, запустивший задание, имеет достаточные права в проекте, а настройки Job token permissions разрешают требуемый доступ.
>
> Если эти условия не выполняются (например, из-за ограничений межпроектного доступа или недостаточной роли), используйте отдельный Project/Personal Access Token со scope `api`.

## Сохранение результата работы

Expand Down
4 changes: 4 additions & 0 deletions pkg/build/stage/instruction/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,7 @@ func ExportUserCommands(stg *User) (backend, source instructions.UserCommand) {
func ExportStopSignalCommands(stg *StopSignal) (backend, source instructions.StopSignalCommand) {
return stg.backendInstruction.StopSignalCommand, *stg.instruction.Data
}

func ExportRunMounts(stg *Run) []*instructions.Mount {
return stg.backendInstruction.GetMounts()
}
10 changes: 10 additions & 0 deletions pkg/build/stage/instruction/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ func (stg *Run) ExpandInstruction(c stage.Conveyor, env map[string]string) error
}
// Setup RUN envs after 2nd stage expansion
stg.backendInstruction.Envs = EnvToSortedArr(stg.GetExpandedEnv(c))

for _, mnt := range instructions.GetMounts(stg.instruction.Data) {
if mnt.From == "" {
continue
}
if ds := stg.instruction.GetDependencyByStageRef(mnt.From); ds != nil {
mnt.From = c.GetImageNameForLastImageStage(stg.TargetPlatform(), ds.GetWerfImageName())
}
}

return nil
}

Expand Down
111 changes: 111 additions & 0 deletions pkg/build/stage/instruction/run_ai_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package instruction_test

import (
"bytes"
"fmt"

"github.com/moby/buildkit/frontend/dockerfile/instructions"
"github.com/moby/buildkit/frontend/dockerfile/parser"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

"github.com/werf/werf/v2/pkg/build/stage"
"github.com/werf/werf/v2/pkg/build/stage/instruction"
)

func parseRunCommandAI(dockerfileText string) *instructions.RunCommand {
p, err := parser.Parse(bytes.NewReader([]byte(dockerfileText)))
Expect(err).To(Succeed())

dockerStages, _, err := instructions.Parse(p.AST)
Expect(err).To(Succeed())
Expect(dockerStages).NotTo(BeEmpty())

for _, cmd := range dockerStages[len(dockerStages)-1].Commands {
if run, ok := cmd.(*instructions.RunCommand); ok {
return run
}
}

Fail("no RUN command found in dockerfile")
return nil
}

func newRunStageAI(runCommand *instructions.RunCommand, dependencyStages []string) *instruction.Run {
return instruction.NewRun(
NewDockerfileStageInstructionWithDependencyStages(runCommand, dependencyStages),
nil, false,
&stage.BaseStageOptions{ImageName: "example-image", ProjectName: "example-project"},
nil, "",
)
}

var _ = Describe("TestAI_ RUN mount from stage resolution", func() {
const resolvedOsImage = "ghcr.io/werf/instruction-test:a71052baf9c6ace8171e59a2ae5ea1aede3fb89aa95d160ec354b205-1661868399091"

It("TestAI_ resolves --mount from=<stage> to the built werf stage image in the backend instruction", func(ctx SpecContext) {
stg := newRunStageAI(
parseRunCommandAI("FROM alpine AS os\nRUN --mount=type=bind,from=os,source=/apk,target=/apk true\n"),
[]string{"os"},
)

conveyor := stage.NewConveyorStub(
stage.NewGiterminismManagerStub(stage.NewLocalGitRepoStub("test"), stage.NewGiterminismInspectorStub()),
map[string]string{"/stage/os": resolvedOsImage},
nil, nil,
)

Expect(stg.ExpandDependencies(ctx, conveyor, map[string]string{})).To(Succeed())

mounts := instruction.ExportRunMounts(stg)
Expect(mounts).To(HaveLen(1))
Expect(mounts[0].From).To(Equal(resolvedOsImage))
})

It("TestAI_ leaves external --mount from=<image> references unchanged", func(ctx SpecContext) {
stg := newRunStageAI(
parseRunCommandAI("FROM alpine\nRUN --mount=type=bind,from=alpine:3.19,source=/etc,target=/etc true\n"),
nil,
)

conveyor := stage.NewConveyorStub(
stage.NewGiterminismManagerStub(stage.NewLocalGitRepoStub("test"), stage.NewGiterminismInspectorStub()),
nil, nil, nil,
)

Expect(stg.ExpandDependencies(ctx, conveyor, map[string]string{})).To(Succeed())

mounts := instruction.ExportRunMounts(stg)
Expect(mounts).To(HaveLen(1))
Expect(mounts[0].From).To(Equal("alpine:3.19"))
})

digestFor := func(ctx SpecContext, resolvedImage string) string {
stg := newRunStageAI(
parseRunCommandAI("FROM alpine AS os\nRUN --mount=type=bind,from=os,source=/apk,target=/apk true\n"),
[]string{"os"},
)

conveyor := stage.NewConveyorStub(
stage.NewGiterminismManagerStub(stage.NewLocalGitRepoStub("test"), stage.NewGiterminismInspectorStub()),
map[string]string{"/stage/os": resolvedImage},
nil, nil,
)

Expect(stg.ExpandDependencies(ctx, conveyor, map[string]string{})).To(Succeed())

digest, err := stg.GetDependencies(ctx, conveyor, stage.NewContainerBackendStub(), nil, nil, nil)
Expect(err).To(Succeed())
return digest
}

It("TestAI_ digest reflects the resolved stage image and is stable for identical inputs", func(ctx SpecContext) {
digest1 := digestFor(ctx, resolvedOsImage)
digest2 := digestFor(ctx, resolvedOsImage)
Expect(digest1).To(Equal(digest2))

changedDigest := digestFor(ctx, "ghcr.io/werf/instruction-test:4930d562bfbee9c931413c826137d49eff6a2e7d39519c1c9488a747-1655913653892")
fmt.Printf("digest: %s, changedDigest: %s\n", digest1, changedDigest)
Expect(changedDigest).NotTo(Equal(digest1))
})
})
8 changes: 4 additions & 4 deletions trdl_channels.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ groups:
- name: "2"
channels:
- name: alpha
version: 2.72.2
version: 2.73.0
- name: beta
version: 2.72.1
version: 2.72.2
- name: ea
version: 2.71.0
version: 2.72.2
- name: stable
version: 2.70.0
version: 2.71.0
- name: rock-solid
version: 2.70.0
Loading