Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
faf8681305 | ||
|
|
047945cabf |
No files matched your search
@@ -20,6 +20,19 @@ regress.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-01: Built images are tagged `upaas-<app>:<short hash>`, git's short
|
||||||
|
form of the commit built, instead of the deployment number. A redeploy of a
|
||||||
|
commit gives its tag to the new image; the old one is kept while the app runs
|
||||||
|
it or Rollback would start it, then removed by its ID, found among the images
|
||||||
|
the app's deployments recorded. The removal of old images now keeps every
|
||||||
|
image any app runs or would roll back to. A deployment's commit is now saved
|
||||||
|
when it is updated, so manual deploys keep the commit read from the clone
|
||||||
|
(#239).
|
||||||
|
|
||||||
|
- 2026-10-01: Two database writes at the same moment no longer fail with
|
||||||
|
"database is locked": each transaction now takes the write lock when it begins
|
||||||
|
and waits up to 5 seconds for another writer to finish (#253).
|
||||||
|
|
||||||
- 2026-10-01: The hint under the app page's environment variable editor now says
|
- 2026-10-01: The hint under the app page's environment variable editor now says
|
||||||
changes take effect at the next deploy or rollback, in the page's warning
|
changes take effect at the next deploy or rollback, in the page's warning
|
||||||
style, instead of asking for a container restart, which keeps the old values
|
style, instead of asking for a container restart, which keeps the old values
|
||||||
@@ -42,15 +55,6 @@ regress.
|
|||||||
create it on the host first. The README has a Volume mounts section saying so
|
create it on the host first. The README has a Volume mounts section saying so
|
||||||
(#235).
|
(#235).
|
||||||
|
|
||||||
- 2026-09-29: Built images are tagged `upaas-<app>:<short hash>`, git's short
|
|
||||||
form of the commit built, instead of the deployment number. A redeploy of a
|
|
||||||
commit gives its tag to the new image; the old one is kept while the app runs
|
|
||||||
it or Rollback would start it, then removed by its ID, found among the images
|
|
||||||
the app's deployments recorded. The removal of old images now keeps every
|
|
||||||
image any app runs or would roll back to. A deployment's commit is now saved
|
|
||||||
when it is updated, so manual deploys keep the commit read from the clone
|
|
||||||
(#239).
|
|
||||||
|
|
||||||
- 2026-09-29: The app page is 50% wider on large screens (84rem instead of
|
- 2026-09-29: The app page is 50% wider on large screens (84rem instead of
|
||||||
56rem), its build log and container log boxes are twice as tall, the build log
|
56rem), its build log and container log boxes are twice as tall, the build log
|
||||||
sits between the webhook URL and the environment variables, and the container
|
sits between the webhook URL and the environment variables, and the container
|
||||||
|
|||||||
@@ -137,8 +137,11 @@ func (d *Database) connect(ctx context.Context) error {
|
|||||||
return fmt.Errorf("failed to create data directory: %w", err)
|
return fmt.Errorf("failed to create data directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open database with WAL mode and foreign keys
|
// Open database with WAL mode and foreign keys. Transactions take the
|
||||||
dsn := dbPath + "?_journal_mode=WAL&_foreign_keys=on"
|
// write lock when they begin and wait up to 5s for another writer,
|
||||||
|
// instead of failing with "database is locked" when both write.
|
||||||
|
dsn := dbPath + "?_journal_mode=WAL&_foreign_keys=on" +
|
||||||
|
"&_txlock=immediate&_busy_timeout=5000"
|
||||||
|
|
||||||
database, err := sql.Open("sqlite3", dsn)
|
database, err := sql.Open("sqlite3", dsn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/docker/docker/pkg/stdcopy"
|
"github.com/docker/docker/pkg/stdcopy"
|
||||||
controlapi "github.com/moby/buildkit/api/services/control"
|
controlapi "github.com/moby/buildkit/api/services/control"
|
||||||
@@ -610,3 +611,72 @@ func TestPerformCloneFailsWithoutShortSHA(t *testing.T) {
|
|||||||
t.Errorf("got error %v, want %v", err, ErrGitCloneFailed)
|
t.Errorf("got error %v, want %v", err, ErrGitCloneFailed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestPerformCloneAsksGitForShortSHA runs a clone of a branch's last commit
|
||||||
|
// and a clone of a given commit against a fake Docker API, and checks that
|
||||||
|
// the command each clone container is created with prints git's own short
|
||||||
|
// form of the commit checked out.
|
||||||
|
func TestPerformCloneAsksGitForShortSHA(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
commitSHA string
|
||||||
|
}{
|
||||||
|
{name: "branch", commitSHA: ""},
|
||||||
|
{name: "commit", commitSHA: cloneCommit},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
created := make(chan container.Config, 1)
|
||||||
|
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.HasSuffix(r.URL.Path, "/containers/create"):
|
||||||
|
var cfg container.Config
|
||||||
|
|
||||||
|
_ = json.NewDecoder(r.Body).Decode(&cfg)
|
||||||
|
created <- cfg
|
||||||
|
|
||||||
|
_, _ = w.Write([]byte(`{"Id":"gitcontainer"}`))
|
||||||
|
case strings.HasSuffix(r.URL.Path, "/wait"):
|
||||||
|
_, _ = w.Write([]byte(`{"StatusCode":0}`))
|
||||||
|
case strings.HasSuffix(r.URL.Path, "/logs"):
|
||||||
|
writeCloneOutput(w)
|
||||||
|
default:
|
||||||
|
_, _ = w.Write([]byte(`{}`))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))
|
||||||
|
t.Cleanup(srv.Close)
|
||||||
|
|
||||||
|
dockerAPI, err := client.NewClientWithOpts(
|
||||||
|
client.WithHost("tcp://" + srv.Listener.Addr().String()),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &Client{docker: dockerAPI, log: slog.Default()}
|
||||||
|
|
||||||
|
cfg := testCloneConfig(t)
|
||||||
|
cfg.commitSHA = tt.commitSHA
|
||||||
|
|
||||||
|
_, err = c.performClone(t.Context(), cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := strings.Join((<-created).Cmd, " ")
|
||||||
|
if !strings.Contains(cmd, "echo SHORT_SHA:$(git rev-parse --short HEAD)") {
|
||||||
|
t.Errorf("clone command %q does not print git's short hash", cmd)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -246,6 +246,54 @@ func TestRecordDeployedImageRemovesOldImages(t *testing.T) {
|
|||||||
assert.False(t, api.forced, "old images must be removed without force")
|
assert.False(t, api.forced, "old images must be removed without force")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestRecordDeployedImageRemovesOnlyTheAppsUntaggedImages runs the step after
|
||||||
|
// a deploy against a fake Docker API that holds three untagged images: one an
|
||||||
|
// earlier deployment of the app recorded, one a deployment of another app
|
||||||
|
// recorded, and one no deployment recorded, such as an image upaas never
|
||||||
|
// built. Only the app's own is removed.
|
||||||
|
func TestRecordDeployedImageRemovesOnlyTheAppsUntaggedImages(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
api := &fakeImageAPI{images: map[string][]string{
|
||||||
|
"sha256:new": {"upaas-myapp:1a2b3c4"},
|
||||||
|
"sha256:myapp": {},
|
||||||
|
"sha256:otherapp": {},
|
||||||
|
"sha256:unknown": {},
|
||||||
|
}}
|
||||||
|
svc, db := newImageTestService(t, api)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
app := saveApp(t, db, "myapp", "", "")
|
||||||
|
otherApp := saveApp(t, db, "otherapp", "", "")
|
||||||
|
|
||||||
|
saveDeployment := func(appID, imageID string) *models.Deployment {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
deployment := models.NewDeployment(db)
|
||||||
|
deployment.AppID = appID
|
||||||
|
deployment.ImageID = sql.NullString{String: imageID, Valid: true}
|
||||||
|
require.NoError(t, deployment.Save(ctx))
|
||||||
|
|
||||||
|
return deployment
|
||||||
|
}
|
||||||
|
|
||||||
|
saveDeployment(app.ID, "sha256:myapp")
|
||||||
|
saveDeployment(otherApp.ID, "sha256:otherapp")
|
||||||
|
deployment := saveDeployment(app.ID, "sha256:new")
|
||||||
|
|
||||||
|
err := svc.RecordDeployedImage(ctx, app, deployment, "sha256:new")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
images, removed := api.state()
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"sha256:myapp"}, removed)
|
||||||
|
assert.Equal(t, map[string][]string{
|
||||||
|
"sha256:new": {"upaas-myapp:1a2b3c4"},
|
||||||
|
"sha256:otherapp": {},
|
||||||
|
"sha256:unknown": {},
|
||||||
|
}, images)
|
||||||
|
}
|
||||||
|
|
||||||
// TestRedeployRemovesImagesLeftWithoutTag deploys commits against a fake
|
// TestRedeployRemovesImagesLeftWithoutTag deploys commits against a fake
|
||||||
// Docker API, some of them again. A build takes the commit's tag from the
|
// Docker API, some of them again. A build takes the commit's tag from the
|
||||||
// image an earlier build of it made. That image is kept, without a tag,
|
// image an earlier build of it made. That image is kept, without a tag,
|
||||||
|
|||||||
Reference in new issue
Block a user