forked from docker-library/docs
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpush.pl
executable file
·240 lines (196 loc) · 9.43 KB
/
push.pl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
#!/usr/bin/env perl
use strict;
use warnings;
use 5.010;
use open ':encoding(utf8)';
use File::Basename qw(basename fileparse);
use File::Temp;
use Getopt::Long;
use Mojo::File;
use Mojo::UserAgent;
use Mojo::Util qw(b64_encode decode encode trim);
use Term::UI;
use Term::ReadLine;
my $hubLengthLimit = 25_000;
my $githubBase = 'https://github.com/docker-library/docs/tree/master'; # TODO point this at the correct "dist-xxx" branch based on "namespace"
my $username;
my $password;
my $batchmode;
my $namespace;
my $logos;
GetOptions(
'u|username=s' => \$username,
'p|password=s' => \$password,
'batchmode!' => \$batchmode,
'namespace=s' => \$namespace,
'logos!' => \$logos,
) or die 'bad args';
die 'no repos specified' unless @ARGV;
my $ua = Mojo::UserAgent->new->max_redirects(10);
$ua->transactor->name('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.125 Safari/537.36');
my $term = Term::ReadLine->new('docker-library-docs-push');
unless (defined $username) {
$username = $term->get_reply(prompt => 'Hub Username');
}
unless (defined $password) {
$password = $term->get_reply(prompt => 'Hub Password'); # TODO hide the input? O:)
}
my $login = $ua->post('https://hub.docker.com/v2/users/login/' => {} => json => { username => $username, password => $password });
die 'login failed' unless $login->res->is_success;
my $token = $login->res->json->{token};
my $csrf;
for my $cookie (@{ $login->res->cookies }) {
if ($cookie->name eq 'csrftoken') {
$csrf = $cookie->value;
last;
}
}
die 'missing CSRF token' unless defined $csrf;
my $attemptLogin = $ua->post('https://hub.docker.com/attempt-login/' => {} => json => { jwt => $token });
die 'attempt-login failed' unless $attemptLogin->res->is_success;
my $authorizationHeader = {
Authorization => "JWT $token",
'X-CSRFToken' => $csrf,
};
my $userData = $ua->get('https://hub.docker.com/v2/user/' => $authorizationHeader);
die 'user failed' unless $userData->res->is_success;
$userData = $userData->res->json;
my $supportedTagsRegex = qr%^(# Supported tags and respective `Dockerfile` links\n\n)(.*?\n)(?=# |\[)%ms;
sub prompt_for_edit {
my $currentText = shift;
my $proposedFile = shift;
my $lengthLimit = shift // 0;
my $proposedText = Mojo::File->new($proposedFile)->slurp // '** FILE MISSING! **';
$proposedText = trim(decode('UTF-8', $proposedText));
# remove our warning about generated files (Hub doesn't support HTML comments in Markdown)
$proposedText =~ s% ^ <!-- .*? --> \s* %%sx;
# extract/re-inject sponsored links
my $sponsoredLinks = '';
if ($currentText =~ m{ ( ^ [#] \Q Sponsored Resources\E \n .*? \n --- \n ) }smx) {
$sponsoredLinks = $1 . "\n";
$proposedText =~ s%$supportedTagsRegex%$sponsoredLinks$1$2%;
}
if ($lengthLimit > 0 && length($proposedText) > $lengthLimit) {
# TODO https://github.com/docker/hub-beta-feedback/issues/238
my $fullUrl = "$githubBase/$proposedFile";
my $shortTags = "-\tSee [\"Supported tags and respective \`Dockerfile\` links\" at $fullUrl]($fullUrl#supported-tags-and-respective-dockerfile-links)\n\n";
my $tagsNote = "**Note:** the description for this image is longer than the Hub length limit of $lengthLimit, so the \"Supported tags\" list has been trimmed to compensate. See [docker/hub-beta-feedback#238](https://github.com/docker/hub-beta-feedback/issues/238) for more information.\n\n" . $shortTags;
my $genericNote = "**Note:** the description for this image is longer than the Hub length limit of $lengthLimit, so has been trimmed. The full description can be found at [$fullUrl]($fullUrl). See [docker/hub-beta-feedback#238](https://github.com/docker/hub-beta-feedback/issues/238) for more information.";
my $startingNote = $genericNote . "\n\n";
my $endingNote = "\n\n...\n\n" . $genericNote;
my $trimmedText = $proposedText;
# if our text is too long for the Hub length limit, let's first try removing the "Supported tags" list and add $tagsNote and see if that's enough to let us put the full image documentation
$trimmedText =~ s%$supportedTagsRegex%$sponsoredLinks$1$tagsNote%ms;
# (we scrape until the next "h1" or a line starting with a link which is likely a build status badge for an architecture-namespace)
if (length($trimmedText) > $lengthLimit) {
# ... if that doesn't do the trick, then do our older naïve description trimming
$trimmedText = $startingNote . substr $proposedText, 0, ($lengthLimit - length($startingNote . $endingNote));
# adding the "ending note" (https://github.com/docker/hub-feedback/issues/2220) is a bit more complicated as we have to deal with cutting off markdown ~cleanly so it renders correctly
# TODO deal with "```foo" appropriately (so we don't drop our note in the middle of a code block) - the Hub's current markdown rendering (2022-04-07) does not auto-close a dangling block like this, so this isn't urgent
if ($trimmedText =~ m/\n$/) {
# if we already end with a newline, we should be fine to just trim newlines and add our ending note
$trimmedText =~ s/\n+$//;
}
else {
# otherwise, we need to get a little bit more creative and trim back to the last fully blank line (which we can reasonably assume is safe thanks to our markdownfmt)
$trimmedText =~ s/\n\n(.\n?)*$//;
}
$trimmedText .= $endingNote;
}
$proposedText = $trimmedText;
}
return $currentText if $currentText eq $proposedText;
my @proposedFileBits = fileparse($proposedFile, qr!\.[^.]*!);
my $file = File::Temp->new(SUFFIX => '-' . basename($proposedFileBits[1]) . '-current' . $proposedFileBits[2]);
my $filename = $file->filename;
Mojo::File->new($filename)->spurt(encode('UTF-8', $currentText . "\n"));
my $tempProposedFile = File::Temp->new(SUFFIX => '-' . basename($proposedFileBits[1]) . '-proposed' . $proposedFileBits[2]);
my $tempProposedFilename = $tempProposedFile->filename;
Mojo::File->new($tempProposedFilename)->spurt(encode('UTF-8', $proposedText . "\n"));
system(qw(git --no-pager diff --no-index), $filename, $tempProposedFilename);
my $reply;
if ($batchmode) {
$reply = 'yes';
}
else {
$reply = $term->get_reply(
prompt => 'Apply changes?',
choices => [ qw( yes vimdiff no quit ) ],
default => 'yes',
);
}
if ($reply eq 'quit') {
say 'quitting, as requested';
exit;
}
if ($reply eq 'yes') {
return $proposedText;
}
if ($reply eq 'vimdiff') {
system('vimdiff', $tempProposedFilename, $filename) == 0 or die "vimdiff on $filename and $proposedFile failed";
return trim(decode('UTF-8', Mojo::File->new($filename)->slurp));
}
return $currentText;
}
while (my $repo = shift) { # 'library/hylang', 'tianon/perl', etc
$repo =~ s!^/+|/+$!!; # trim extra slashes (from "*/" globbing, for example)
$repo = $namespace . '/' . $repo if $namespace; # ./push.pl --namespace xxx ...
$repo = 'library/' . $repo unless $repo =~ m!/!; # "hylang" -> "library/hylang"
my $repoName = $repo;
$repoName =~ s!^.*/!!; # 'hylang', 'perl', etc
my $repoUrl = 'https://hub.docker.com/v2/repositories/' . $repo . '/';
if ($logos && $repo =~ m{ ^ library/ }x) {
# the "library" org images include a logo which is displayed in the Hub UI
# if we have a logo file, let's update that metadata first
my $repoLogo120 = $repoName . '/logo-120.png';
if (!-f $repoLogo120) {
my $repoLogoPng = $repoName . '/logo.png';
my $repoLogoSvg = $repoName . '/logo.svg';
my $logoToConvert = (
-f $repoLogoPng
? $repoLogoPng
: $repoLogoSvg
);
if (-f $logoToConvert) {
say 'converting ' . $logoToConvert . ' to ' . $repoLogo120;
system(
qw( convert -background none -density 1200 -strip -resize 120x120> -gravity center -extent 120x120 ),
$logoToConvert,
$repoLogo120,
) == 0 or die "failed to convert $logoToConvert into $repoLogo120";
}
}
if (-f $repoLogo120) {
my $proposedLogo = Mojo::File->new($repoLogo120)->slurp;
my $currentLogo = $ua->get('https://d1q6f0aelx0por.cloudfront.net/product-logos/' . join('-', split(m{/}, $repo)) . '-logo.png', { 'Cache-Control' => 'no-cache' });
$currentLogo = ($currentLogo->res->is_success ? $currentLogo->res->body : undef);
if ($currentLogo && $currentLogo eq $proposedLogo) {
say 'no change to ' . $repoName . ' logo; skipping';
}
else {
say 'putting logo ' . $repoLogo120;
my $logoUrl = $repoUrl . 'logo';
my $logoPut = $ua->put($logoUrl => $authorizationHeader => json => {
'image_data' => b64_encode($proposedLogo),
'content_type' => 'image/png',
'file_ext' => 'png',
});
warn 'warning: put to ' . $logoUrl . ' failed: ' . $logoPut->res->text unless $logoPut->res->is_success;
}
}
}
my $repoTx = $ua->get($repoUrl => $authorizationHeader);
warn 'warning: failed to get: ' . $repoUrl . ' (skipping)' and next unless $repoTx->res->is_success;
my $repoDetails = $repoTx->res->json;
$repoDetails->{description} //= '';
$repoDetails->{full_description} //= '';
my $hubShort = prompt_for_edit($repoDetails->{description}, $repoName . '/README-short.txt');
my $hubLong = prompt_for_edit($repoDetails->{full_description}, $repoName . '/README.md', $hubLengthLimit);
say 'no change to ' . $repoName . '; skipping' and next if $repoDetails->{description} eq $hubShort and $repoDetails->{full_description} eq $hubLong;
say 'updating ' . $repoName;
my $repoPatch = $ua->patch($repoUrl => $authorizationHeader => json => {
description => $hubShort,
full_description => $hubLong,
});
warn 'patch to ' . $repoUrl . ' failed: ' . $repoPatch->res->text and next unless $repoPatch->res->is_success;
}